自定義具有拉伸阻尼效果的ScrollerView
引言
一切的自定義都是來(lái)自于需求,而在項(xiàng)目開(kāi)發(fā)中由于界面條目太多,所以自然而然的使用到了ScrollerView瑟捣,當(dāng)把效果給產(chǎn)品經(jīng)理的時(shí)候呢末荐,ios和Android的效果完全不一樣,ios自帶的上下拉伸回彈的效果勃救,而Android沒(méi)有碍讨,所以自定義一個(gè)具有拉伸效果的ScrollerView迫在眉睫啊.
首先來(lái)看一下效果圖,妹子很漂亮蒙秒,但是注意重點(diǎn)2颉!晕讲!
好了覆获,下面進(jìn)入正題,通過(guò)繼承ScrollerView來(lái)進(jìn)行相關(guān)滑動(dòng)回彈的效果實(shí)現(xiàn)瓢省,先來(lái)定義幾個(gè)變量:
private View childView;// 子View(ScrollerView的唯一子類)
private int y;// 點(diǎn)擊時(shí)y坐標(biāo)
private Rect rect = new Rect();// 矩形(用來(lái)保存inner的初始狀態(tài)弄息,判斷是夠需要?jiǎng)赢?huà)回彈效果)
注釋打的也很清楚,然后我們先在該ScrollerView的xml布局加載完成后獲取ScrollerView的唯一子布局賦值給上面定義的childView:
/**
* 在xml布局繪制為界面完成時(shí)調(diào)用勤婚,
* 獲取ScrollerView中唯一的直系子布局(ScrollerView中不許包含一層ViewGroup摹量,有且只有一個(gè))
*/
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
childView = getChildAt(0);
}
super.onFinishInflate();
}
下面就是處理Touch事件了:
/**
* touch 事件處理
**/
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (childView != null) {
handleTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
/***
* 觸摸事件
*
* @param ev
*/
public void handleTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
y = (int) ev.getY();//按下的時(shí)候獲取到y(tǒng)坐標(biāo)
break;
case MotionEvent.ACTION_MOVE:
int nowY = (int) ev.getY(); // 移動(dòng)時(shí)的實(shí)時(shí)y坐標(biāo)
int delayY = y - nowY; // 移動(dòng)時(shí)的間隔
y = nowY; // 將本次移動(dòng)結(jié)束時(shí)的y坐標(biāo)賦值給下次移動(dòng)的起始坐標(biāo)(也就是nowY)
if (isNeedMove()) {
if (rect.isEmpty()) {
//rect保存childView的初始位置信息
rect.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
}
//移動(dòng)布局(屏幕移動(dòng)的距離等于手指滑動(dòng)距離的一般)
childView.layout(childView.getLeft(), childView.getTop() - delayY / 2, childView.getRight(), childView.getBottom() - delayY / 2);
}
break;
case MotionEvent.ACTION_UP:
if (isNeedAnimation()) {// 判斷rect是否為空,也就是是否被重置了
startAnim();//開(kāi)始回彈動(dòng)畫(huà)
}
break;
default:
break;
}
}
對(duì)于Touch事件的處理,我注釋說(shuō)的應(yīng)該很清楚缨称,但是里面有需要調(diào)用的四個(gè)方法:
- 判斷布局是否需要移動(dòng)
/**
* 判斷布局是否需要移動(dòng)
* @return
*/
private boolean isNeedMove() {
int offset = childView.getMeasuredHeight() - getHeight();
int scrollY = getScrollY();
// 0是頂部凝果,后面那個(gè)是底部(需要仔細(xì)想一下這個(gè)過(guò)程)
if (scrollY == 0 || scrollY == offset) {
return true;
}
return false;
}
其中childView.getMeasuredHeight()為獲取到該布局的實(shí)際高度,getHeight是該布局在屏幕中顯示的高度睦尽,getScrollY()是滑動(dòng)的時(shí)候相對(duì)于起始位置的距離器净。
- 判斷rect是否為空
在加載布局的時(shí)候rect進(jìn)行了初始化,當(dāng)確定需要滑動(dòng)時(shí)骂删,再判斷一下rect是否為空掌动,因?yàn)樵搑ect在布局執(zhí)行動(dòng)畫(huà)回彈之后就會(huì)被置空,如果當(dāng)Scroller頂部對(duì)其或者底部對(duì)其宁玫,未在回彈過(guò)程就會(huì)將該時(shí)刻Scroller的位置信息傳入到rect粗恢,方便回彈的時(shí)候根據(jù)rect保存的scrollerview的位置信息完成回彈作用。
- 判斷是否需要?jiǎng)赢?huà)
public boolean isNeedAnimation() {
return !rect.isEmpty();
}
- 執(zhí)行動(dòng)畫(huà)回彈
private void startAnim() {
TranslateAnimation anim = new TranslateAnimation(0, 0, childView.getTop(), rect.top);
anim.setDuration(200);
anim.setInterpolator(new OvershootInterpolator());//加速器
childView.startAnimation(anim);
// 將inner布局重新回到起始位置
childView.layout(rect.left, rect.top, rect.right, rect.bottom);
rect.setEmpty();
}
在執(zhí)行完動(dòng)畫(huà)回彈后即ScrollerView回歸了原始狀態(tài)欧瘪,于是rect也就置空眷射,方便下一次繼續(xù)記錄,好了佛掖,下面直接貼一份完整代碼吧:
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;
/**
* Author : luweicheng on 2017/7/20 0020 15:15
* E-mail :1769005961@qq.com
* GitHub : https://github.com/luweicheng24
* funcation: 具有拉伸效果的ScrollerView
*/
public class CustomScroller extends ScrollView {
private View childView;// 子View(ScrollerView的唯一子類)
private int y;// 點(diǎn)擊時(shí)y坐標(biāo)
private Rect rect = new Rect();// 矩形(用來(lái)保存inner的初始狀態(tài)妖碉,判斷是夠需要?jiǎng)赢?huà)回彈效果)
public CustomScroller(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 在xml布局繪制為界面完成時(shí)調(diào)用,
* 獲取ScrollerView中唯一的直系子布局(ScrollerView中不許包含一層ViewGroup芥被,有且只有一個(gè))
*/
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
childView = getChildAt(0);
}
super.onFinishInflate();
}
/**
* touch 事件處理
**/
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (childView != null) {
handleTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
/***
* 觸摸事件
*
* @param ev
*/
public void handleTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
y = (int) ev.getY();//按下的時(shí)候獲取到y(tǒng)坐標(biāo)
break;
case MotionEvent.ACTION_MOVE:
int nowY = (int) ev.getY(); // 移動(dòng)時(shí)的實(shí)時(shí)y坐標(biāo)
int delayY = y - nowY; // 移動(dòng)時(shí)的間隔
y = nowY; // 將本次移動(dòng)結(jié)束時(shí)的y坐標(biāo)賦值給下次移動(dòng)的起始坐標(biāo)(也就是nowY)
if (isNeedMove()) {
if (rect.isEmpty()) {
//rect保存childView的初始位置信息
rect.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
}
//移動(dòng)布局(屏幕移動(dòng)的距離等于手指滑動(dòng)距離的一般)
childView.layout(childView.getLeft(), childView.getTop() - delayY / 2, childView.getRight(), childView.getBottom() - delayY / 2);
}
break;
case MotionEvent.ACTION_UP:
if (isNeedAnimation()) {// 判斷rect是否為空欧宜,也就是是否被重置了
startAnim();//開(kāi)始回彈動(dòng)畫(huà)
}
break;
default:
break;
}
}
private void startAnim() {
TranslateAnimation anim = new TranslateAnimation(0, 0, childView.getTop(), rect.top);
anim.setDuration(200);
anim.setInterpolator(new OvershootInterpolator());//加速器
childView.startAnimation(anim);
// 將inner布局重新回到起始位置
childView.layout(rect.left, rect.top, rect.right, rect.bottom);
rect.setEmpty();
}
/**
* 判斷布局是否需要移動(dòng)
* @return
*/
private boolean isNeedMove() {
int offset = childView.getMeasuredHeight() - getHeight();
int scrollY = getScrollY();
// 0是頂部,后面那個(gè)是底部(需要仔細(xì)想一下這個(gè)過(guò)程)
if (scrollY == 0 || scrollY == offset) {
return true;
}
return false;
}
public boolean isNeedAnimation() {
return !rect.isEmpty();
}
}
如果有問(wèn)題拴魄,在下面留言冗茸,共同探討。