前言
上一個(gè)周沒(méi)有寫博客凑兰,是我太懶雳窟,無(wú)法堅(jiān)持负间。在上一個(gè)周苛秕,除去工作的任務(wù)(迭代版本唯笙,修復(fù)BUG)之外螟蒸,我一直在模仿一個(gè)UI效果。我呢崩掘,算是一個(gè)米粉七嫌,我前面的博客,有一些效果就是來(lái)自MIUI苞慢。在MIUI中诵原,很多的列表都具有彈性和粘性,個(gè)人覺(jué)得這個(gè)效果不錯(cuò),于是就模仿了一下绍赛。
本來(lái)開始之初是為了將這個(gè)效果封裝成為一個(gè)單獨(dú)的UI控件蔓纠,結(jié)果寫著寫著就發(fā)現(xiàn)這樣是不合理的,于是就放在一旁等待解決方案吗蚌,先看看實(shí)際的效果吧腿倚。(前面兩個(gè)是我項(xiàng)目中的實(shí)際效果)
怎么樣,看上去效果還是挺可以的吧褪测,不得不說(shuō)猴誊,MIUI在一些小細(xì)節(jié)上面做得非常不錯(cuò),很多效果都值得我們深入的進(jìn)行學(xué)習(xí)侮措。
注意
- 本博客最主要的是為了向大家展示一種解決思路懈叹,文章中的類表效果用到實(shí)際項(xiàng)目中還是有些許問(wèn)題。
- 為了方便起見(jiàn)分扎,本文中使用的動(dòng)畫效果來(lái)自于JakeWharton大神的NineOldAndroids的支持庫(kù)澄成,個(gè)人非常不建議新手直接就來(lái)使用開源庫(kù),最起碼應(yīng)該熟悉一些基礎(chǔ)畏吓。
分析
剛開始的時(shí)候墨状,我一直在網(wǎng)上找類似的效果,一直是沒(méi)有找到菲饼。直到我學(xué)習(xí)完屬性動(dòng)畫之后才發(fā)現(xiàn)肾砂,其實(shí)這個(gè)效果實(shí)現(xiàn)起來(lái)是非常的簡(jiǎn)單。
-
整個(gè)效果看上去分為上拉和下來(lái)宏悦,上拉和下拉的時(shí)候進(jìn)行縮放镐确。
1. 下拉: 將View的中心點(diǎn)移到(width/2,0)中進(jìn)行Scale縮放 2. 上拉 將View的中心點(diǎn)移到(width/2,height)中進(jìn)行Scale縮放
松手之后會(huì)有一個(gè)回彈效果,使用ValueAnimator來(lái)進(jìn)行散發(fā)scale值饼煞,采用OvershootInterpolator差值器就能達(dá)到這樣的效果源葫。
編碼
1. 選擇繼承
自定義View又幾種方式:
- 繼承自View實(shí)現(xiàn)效果。
- 繼承原生控件進(jìn)行拓展砖瞧。
- 組合控件息堂。
很明顯,效果圖中都是包含了子控件的块促,可以選擇繼承ViewGroup來(lái)實(shí)現(xiàn)荣堰,但是我根本不關(guān)心子控件的一些測(cè)量和layout,所以需要繼承已經(jīng)實(shí)現(xiàn)的ViewGroup竭翠。最后我選定的是使用ScrollView持隧,原因是為了兼容滾動(dòng),并且需要監(jiān)聽(tīng)是否已經(jīng)滾動(dòng)到了底部逃片。
2.準(zhǔn)備工作
- 創(chuàng)建項(xiàng)目
- 引用開源庫(kù) compile 'com.nineoldandroids:library:2.4.0'
- 創(chuàng)建自定義控件類繼承ScrollView屡拨,實(shí)現(xiàn)三個(gè)構(gòu)造方法只酥,并且在xml中引用
3. 自定義屬性
首先思考我們需要哪一些屬性,比方說(shuō)手指抬起后回彈的速度呀狼,回彈的效果方式(其實(shí)就是不同的差值器)裂允,能夠進(jìn)行果凍縮放的方式,只能是頂部哥艇、底部或者不限制绝编。
在value文件夾中創(chuàng)建attr.xml
<attr name="BouncingDuration" format="integer" />
<attr name="BouncingInterpolator" format="enum">
<enum name="OvershootInterpolator" value="1" />
<enum name="BounceInterpolator" value="2" />
<enum name="LinearInterpolator" value="3" />
<enum name="AccelerateDecelerateInterpolator" value="4" />
</attr>
<attr name="BouncingType" format="enum">
<enum name="none" value="0" />
<enum name="top" value="1" />
<enum name="bottom" value="2" />
<enum name="both" value="3" />
</attr>
<declare-styleable name="BouncingJellyScrollView">
<attr name="BouncingDuration" />
<attr name="BouncingInterpolator" />
<attr name="BouncingType" />
</declare-styleable>
將attr獨(dú)立出來(lái)的原因是我還有幾個(gè)控件需要使用相同的一些屬性。
5. 初始化
在構(gòu)造方法中初始化一些常量值和屬性貌踏。
其它的一些工具類方法
public class BouncingType {
public static final int NONE = 0;
public static final int TOP = 1;
public static final int BOTTOM = 2;
public static final int BOTH = 3;
}
public class BouncingInterpolatorType {
public static final int OVERSHOOT_INTERPOLATOR = 1;
public static final int BOUNCE_INTERPOLATOR = 2;
public static final int LINEAR_INTERPOLATOR = 3;
public static final int ACCELERATE_DECELERATE_INTERPOLATOR = 4;
/**
* 獲取彈跳類型
*
* @return
*/
public static TimeInterpolator getTimeInterpolator(int type) {
TimeInterpolator mTimeInterpolator = null;
switch (type) {
case OVERSHOOT_INTERPOLATOR:
mTimeInterpolator = new OvershootInterpolator();
break;
case BOUNCE_INTERPOLATOR:
mTimeInterpolator = new BounceInterpolator();
break;
case LINEAR_INTERPOLATOR:
mTimeInterpolator = new LinearInterpolator();
break;
case ACCELERATE_DECELERATE_INTERPOLATOR:
mTimeInterpolator = new AccelerateDecelerateInterpolator();
break;
}
return mTimeInterpolator;
}
}
初始化屬性
/**
* @param attrs
*/
private void initAttr(AttributeSet attrs) {
if (attrs != null) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.BouncingJellyScrollView);
//差值器
mTimeInterpolator = BouncingInterpolatorType.getTimeInterpolator(typedArray.getInteger(
R.styleable.BouncingJellyScrollView_BouncingInterpolator
, BouncingInterpolatorType.OVERSHOOT_INTERPOLATOR));
//回彈速度
mBouncingDuration = typedArray.getInteger(R.styleable.BouncingJellyScrollView_BouncingDuration, mBouncingDuration);
//果凍類型
mBouncingType = typedArray.getInt(R.styleable.BouncingJellyScrollView_BouncingType, BouncingType.BOTH);
typedArray.recycle();
//獲取是差值 整個(gè)屏幕的三倍大小
bouncingOffset=ScreenUtils.getScreenHeight(getContext()) * 3;
}
}
onSizeChanged中驗(yàn)證模式
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//判斷可滾動(dòng)的內(nèi)容是不是小于整個(gè)屏幕的高度十饥,以防底部進(jìn)行所動(dòng)
int contentHeight = getChildAt(0).getHeight();
if (contentHeight > 0 && contentHeight <= ScreenUtils.getScreenHeight(getContext())) {
mBouncingType = BouncingType.TOP;
}
}
4. 開始編碼
因?yàn)槲覀兝^承的是ViewGroup,子View還需要攔截事件祖乳,所以我們需要重寫dispatchTouchEvent方法逗堵,并且在其中攔截事件分發(fā)和做縮放效果。
先實(shí)現(xiàn)在頂部進(jìn)行滑動(dòng)的時(shí)候隨著手指移動(dòng)而進(jìn)行縮放
/**
* 從頂部開始滑動(dòng)
*/
public void bouncingTo() {
//設(shè)置X坐標(biāo)點(diǎn)
ViewHelper.setPivotX(this, getWidth() / 2);
//設(shè)置Y坐標(biāo)點(diǎn)
ViewHelper.setPivotY(this, 0);
//進(jìn)行縮放
ViewHelper.setScaleY(this, 1.0f + offsetScale);
}
/**
* 從頂部開始滑動(dòng)
*/
public void bouncingBottom() {
//設(shè)置X坐標(biāo)點(diǎn)
ViewHelper.setPivotX(this, getWidth() / 2);
//設(shè)置Y坐標(biāo)點(diǎn)
ViewHelper.setPivotY(this, getHeight());
ViewHelper.setScaleY(this, 1.0f + offsetScale);
}
在ACTION_DOWN記錄按下的坐標(biāo)眷昆,用于計(jì)算縮放值和進(jìn)行回彈蜒秤。因?yàn)锳CTION_DOWN事件必定會(huì)傳遞到子view的,所以不能直接返回true亚斋。
//移動(dòng)坐標(biāo)
dowY = (int) event.getRawY();
//按下坐標(biāo) 用于計(jì)算縮放值
dowY2 = (int) event.getRawY();
在ACTION_MOVE中進(jìn)行事件分發(fā)和縮放作媚。
-
實(shí)現(xiàn)頂部滑動(dòng)縮放,主要原理是判斷當(dāng)前是不是滾動(dòng)到了頂部帅刊,獲取手指移動(dòng)的方向和距離纸泡。
moveX = (int) event.getRawX();
moveY = (int) event.getRawY();
//dy值 判斷方向
int dy = moveY - dowY;
dowY = moveY;
//頂部
if (dy > 0 && getScrollY() == 0) {
//判斷果凍的類型
if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
//獲取現(xiàn)在坐標(biāo)與按下坐標(biāo)的差值
int abs = moveY - dowY2;
//計(jì)算縮放值
offsetScale = (Math.abs(abs) / bouncingOffset);
if (offsetScale > 0.3f) {
offsetScale = 0.3f;
}
isTop = true;
bouncingTo();
return true;
}
}
實(shí)現(xiàn)第一步效果如下:
- 回拉恢復(fù)
效果是出來(lái)了,從頂部下拉的時(shí)候慢慢的縮放了赖瞒,但是如果在下拉一定距離后上拉會(huì)是怎么樣的呢女揭?應(yīng)該是慢慢的縮回去,然后再進(jìn)行滾動(dòng)冒黑。 需要在頂部if后面再加上判斷
if (getScrollY() == 0 && dy < 0 && offsetScale > 0) {//為頂部 并且dy為下拉 并且縮放值大于0
if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
//獲取現(xiàn)在坐標(biāo)與按下坐標(biāo)的差值
int abs = moveY - dowY2;
//計(jì)算縮放值
offsetScale = (Math.abs(abs) / bouncingOffset);
if (offsetScale > 0.3f) {
offsetScale = 0.3f;
}
if (abs <= 0) {
offsetScale = 0;
dowY2 = moveY;
}
isTop = true;
bouncingTo();
return true;
}
}
效果如下:
- 手指抬起進(jìn)行回彈
前面兩步完成了整個(gè)拉取的過(guò)程,現(xiàn)在只要加上手機(jī)抬起的時(shí)候進(jìn)行回彈就可以了勤哗。整個(gè)回彈過(guò)程是有一個(gè)時(shí)間段抡爹,并且還有一個(gè)效果。采用ValueAnimator來(lái)散發(fā)offsetScale值來(lái)不斷的改變縮放值就能達(dá)到效果芒划。
ACTION_UP代碼
if (mBouncingType != BouncingType.NONE) {
if (offsetScale > 0) {
backBouncing(offsetScale, 0);
return true;
}
}
/**
* 進(jìn)行回彈
*
* @param from
* @param to
*/
private void backBouncing(final float from, final float to) {
//初始化
if (animator != null && animator.isRunning()) {
animator.cancel();
animator = null;
offsetScale = 0;
bouncingTo();
}
if (mTimeInterpolator == null) {
mTimeInterpolator = new OvershootInterpolator();
}
//散發(fā)值
animator = ValueAnimator.ofFloat(from, to).setDuration(mBouncingDuration);
animator.setInterpolator(mTimeInterpolator);//差值器
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//獲取動(dòng)畫階段的值
offsetScale = (float) animation.getAnimatedValue();
if (isTop) {//回彈到頂部
bouncingTo();
} else {//回彈到底部
bouncingBottom();
}
}
});
animator.start();
}
效果如下:
其實(shí)到這里冬竟,整個(gè)果凍視圖就已經(jīng)算是完成了,至于底部滑動(dòng)民逼,縮放都是一樣的泵殴,只是方向,值相反而已拼苍。判斷是否已經(jīng)滾動(dòng)到了底部笑诅,判斷方向等。以下附上dispatchTouchEvent的代碼,代碼量有些冗余吆你,只是為了每個(gè)部分的清晰而已弦叶。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//移動(dòng)坐標(biāo)
dowY = (int) event.getRawY();
//按下坐標(biāo) 用于計(jì)算縮放值
dowY2 = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
if (mBouncingType != BouncingType.NONE) {
moveX = (int) event.getRawX();
moveY = (int) event.getRawY();
//dy值 判斷方向
int dy = moveY - dowY;
dowY = moveY;
//頂部
if (dy > 0 && getScrollY() == 0) {
//判斷果凍的類型
if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
//獲取現(xiàn)在坐標(biāo)與按下坐標(biāo)的差值
int abs = moveY - dowY2;
//計(jì)算縮放值
offsetScale = (Math.abs(abs) / bouncingOffset);
if (offsetScale > 0.3f) {
offsetScale = 0.3f;
}
isTop = true;
bouncingTo();
return true;
}
} else if (getScrollY() == 0 && dy < 0 && offsetScale > 0) {//為頂部 并且dy為下拉 并且縮放值大于0
if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
//獲取現(xiàn)在坐標(biāo)與按下坐標(biāo)的差值
int abs = moveY - dowY2;
//計(jì)算縮放值
offsetScale = (Math.abs(abs) / bouncingOffset);
if (offsetScale > 0.3f) {
offsetScale = 0.3f;
}
if (abs <= 0) {
offsetScale = 0;
dowY2 = moveY;
}
isTop = true;
bouncingTo();
return true;
}
}
//底部
if (dy < 0 && getScrollY() + getHeight() >= computeVerticalScrollRange()) {//滾動(dòng)到底部
if (mBouncingType == BouncingType.BOTTOM || mBouncingType == BouncingType.BOTH) {
int abs = moveY - dowY2;
offsetScale = (Math.abs(abs) / bouncingOffset);
if (offsetScale > 0.3f) {
offsetScale = 0.3f;
}
isTop = false;
bouncingBottom();
}
} else if (dy > 0 && getScrollY() + getHeight() >= computeVerticalScrollRange() && offsetScale > 0) {
if (mBouncingType == BouncingType.BOTTOM || mBouncingType == BouncingType.BOTH) {
int abs = moveY - dowY2;
offsetScale = (Math.abs(abs) / bouncingOffset);
if (offsetScale > 0.3f) {
offsetScale = 0.3f;
}
if (abs >= 0) {
offsetScale = 0;
dowY2 = moveY;
}
isTop = false;
bouncingBottom();
return true;
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mBouncingType != BouncingType.NONE) {
if (offsetScale > 0) {
backBouncing(offsetScale, 0);
return true;
}
}
break;
}
return super.dispatchTouchEvent(event);
}
來(lái)一個(gè)整體完成的效果圖:
其它View
- RecyclerView,ListVIew實(shí)現(xiàn)的原理都是一樣的妇多,判斷是否在頂部伤哺,滑動(dòng)方向等,再進(jìn)行縮放即可者祖。另外我實(shí)現(xiàn)了一個(gè)RecycerView的demo立莉,代碼和上面的基本上一致。
最后
最后七问,這暫時(shí)是一個(gè)最基本的蜓耻,等有時(shí)間我會(huì)繼續(xù)完成這個(gè)自定義View的。如果可能烂瘫,希望大家能夠給我一個(gè)star媒熊。