參考資料
郭霖 Scroller完全解析
鴻洋 ViewDragHelper完全解析
鴻洋 ViewDragHelper實戰(zhàn) 自己打造Drawerlayout
-目錄
- 1)layout
- 2)offsetLeftAndRight() offsetTopAndBottom()
- 3)LayoutParams()
- 4)scrollTo() scrollBy()
- 5)Scroller
- 6)屬性動畫
- 7)ViewDragHelper
-實現(xiàn)滑動的7種方法
public class DragView extends View {
private static final String TAG = "DragView";
private int lastX, lastY;
private Scroller scroller;
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(context);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
//方法一
// layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//方法二
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
//方法三
// LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
// layoutParams.leftMargin = getLeft()+offsetX;
// layoutParams.topMargin = getTop()+offsetY;
// setLayoutParams(layoutParams);
//方法四
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
case MotionEvent.ACTION_UP:
View view = (View)getParent();
Log.i(TAG, "getScrollX: "+view.getScrollX());
Log.i(TAG, "getScrollY: "+view.getScrollY());
scroller.startScroll(view.getScrollX(),view.getScrollY(),-view.getScrollX(),-view.getScrollY());
invalidate();
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()){
Log.i(TAG, "getCurrX: "+scroller.getCurrX());
Log.i(TAG, "getCurrY: "+scroller.getCurrY());
((ViewGroup)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
invalidate();
}
}
}
1) layout
2) offsetLeftAndRight() offsetTopAndBottom()
3) LayoutParams()
//使用MarginLayoutParams更加方便還不用考慮父布局是LinearLayout還是RelativeLayout
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft()+offsetX;
layoutParams.rightMargin = getRight()+offsetY;
setLayoutParams(layoutParams);
4) scrollTo() scrollBy()
任何一個控件都是可以滾動的,因為View類中有scrollTo()和scrollBy()兩個方法左电,scrollBy()是讓View相對于當前位置滾動某段距離踪古,scrollTo()是讓View相對于初始位置滾動某段距離。
scrollTo,scrollBy方法移動的是View的內(nèi)容券腔,如果ViewGroup中使用scrollTo纷纫,scrollBy参滴,那么移動的將是所有子View暴心。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
layout = (LinearLayout) findViewById(R.id.layout);
scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);
scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);
scrollToBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layout.scrollTo(-60, -100); //注意此處是layout的scrollTo()
}
});
scrollByBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layout.scrollBy(-60, -100);//注意此處是layout的scrollBy()
}
});
}
下圖中為什么scrollBy(-60, -100)策橘,按鈕確是向手機坐標系的x和y軸正向移動呢辰斋?
答:可以想象屏幕是一個放大鏡藕夫,而下面是一個巨大的畫布炫加,使用scrollBy方法,將layout向X軸負方向(左)平移60饮六,向Y軸負方向(上)平移100,則layout內(nèi)的子view相當于向X軸和Y軸的正方向上移動了橘霎。
5) Scroller
使用Scroller模仿ViewPager的例子
startScroll(int startX,int startY,int dx, int dy,int duration)
startScroll(int startX,int startY,int dx, int dy)
/**
* Created by 涂高峰 on 2017/6/21.
*/
public class ScrollerLayout extends ViewGroup {
private static final String TAG = "ScrollerLayout";
private Scroller mScroller;
private int mDownX,mMoveX;
private int leftBorder,rightBorder;
private int mTouchSlop;
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
//大于這個距離,系統(tǒng)認為是移動
mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i=0; i<count; i++){
View child = getChildAt(i);
measureChild(child,widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i=0; i<count; i++){
View child = getChildAt(i);
child.layout(i*child.getMeasuredWidth(), 0, (i+1)*child.getMeasuredWidth(), child.getMeasuredHeight());
}
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(getChildCount()-1).getRight();
Log.i(TAG, "leftBorder: "+leftBorder);
Log.i(TAG, "rightBorder: "+rightBorder);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mDownX = x;
mMoveX = x;
break;
case MotionEvent.ACTION_MOVE:
//按下的坐標與當前移動坐標絕對值 大于 系統(tǒng)默認的移動距離
//攔截此移動事件,不向子view傳遞,進入自身的onTouchEvent
if (Math.abs(mDownX - x)>mTouchSlop){
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//如果子控件為Button之類的clickable控件,則會由button消費掉down事件,當viewgroup滑動時,會攔截move事件并處理
//但是若子控件為TextView之類的非clickable控件,則viewgroup和textview都不會消費掉down事件.
//由于沒有任何view消費down事件,后續(xù)事件將由上層消費,而不會往下傳遞給viewgroup.所以此處需要將down事件消費掉,從而能繼續(xù)接收后續(xù)事件
return true;
case MotionEvent.ACTION_MOVE:
//偏移量
int offsetX = mMoveX-x;
//左邊界處理
if (getScrollX()+offsetX < leftBorder){
scrollTo(leftBorder,0);
return true;
}
//右邊界處理
if (getScrollX()+offsetX + getWidth()> rightBorder){
scrollTo(rightBorder-getWidth(),0);
return true;
}
//滑動處理
scrollBy(offsetX,0);
mMoveX = x;
break;
case MotionEvent.ACTION_UP:
//手指抬起,判斷是哪個子控件的index
//小于第一個子控件的一半寬度則認為是第一個子控件
//大于第一個子控件的一半寬度則認為是下一個子控件
int index = (getScrollX()+getWidth()/2)/getWidth();
Log.i(TAG, "index: "+index); //結(jié)果為 0 1 2
//根據(jù)子空間index計算偏移量
int dy = index * getWidth() - getScrollX();
Log.i(TAG, "dy: "+dy);
mScroller.startScroll(getScrollX(),0,dy,0);
invalidate();
break;
}
return super.onTouchEvent(event);
}
//重繪會調(diào)用此方法,此方法中的invalidate又會觸發(fā)重繪,從而循環(huán)實現(xiàn)彈性滑動
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
}
6) 屬性動畫(動畫中講解)
7) ViewDragHelper
在自定義ViewGroup中延曙,很多效果都包含用戶手指去拖動其內(nèi)部的某個View(eg:側(cè)滑菜單等),針對具體的需要去寫好onInterceptTouchEvent和onTouchEvent這兩個方法是一件很不容易的事顿涣,需要自己去處理:多手指的處理揉阎、加速度檢測等等。
好在官方在v4的支持包中提供了ViewDragHelper這樣一個類來幫助我們方便的編寫自定義ViewGroup
1)ViewDragHelper類相關(guān)的API:
方法 | 說明 |
---|---|
create(ViewGroup forParent, ViewDragHelper.Callback cb) | 創(chuàng)建viewDragHelper |
captureChildView(View childView, int activePointerId) | 捕獲子視圖 |
checkTouchSlop(int directions, int pointerId) | 檢查移動是否為最小的滑動速度 |
findTopChildUnder(int x, int y) | 返回指定位置上的頂部子視圖 |
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) | 解決捕獲視圖自由滑動的位置 |
getActivePointerId() | 獲取活動的子視圖的id |
getCapturedView() | 獲取捕獲的視圖 |
getEdgeSize() | 獲取邊界的大小 |
getMinVelocity() | 獲取最小的速度 |
getTouchSlop() | 獲取最小的滑動速度 |
getViewDragState() | 獲取視圖的拖動狀態(tài) |
isCapturedViewUnder(int x, int y) | 判斷該位置是否為捕獲的視圖 |
isEdgeTouched(int edges) | 判斷是否為邊界觸碰 |
setEdgeTrackingEnabled(int edgeFlags) | 設(shè)置邊界跟蹤 |
settleCapturedViewAt(int finalLeft, int finalTop) | 設(shè)置捕獲的視圖到指定的位置 |
smoothSlideViewTo(View child, int finalLeft, int finalTop) | 滑動側(cè)邊欄到指定的位置 |
shouldInterceptTouchEvent(MotionEvent ev) | 處理父容器是否攔截事件 |
processTouchEvent(MotionEvent ev) | 處理父容器攔截的事件 |
2)ViewDragHelper.Callback相關(guān)API:
方法 | 說明 |
---|---|
clampViewPositionHorizontal(View child, int left, int dx) | 控制橫軸的移動距離 |
clampViewPositionVertical(View child, int top, int dy) | 控制縱軸的移動距離 |
getViewHorizontalDragRange(View child) | 獲取視圖在橫軸移動的距離 |
getViewVerticalDragRange(View child) | 獲取視圖在縱軸的移動距離 |
onEdgeDragStarted(int edgeFlags, int pointerId) | 處理當用戶觸碰邊界移動開始的回調(diào) |
onEdgeLock(int edgeFlags) | 處理邊界被鎖定時的回調(diào) |
onEdgeTouched(int edgeFlags, int pointerId) | 處理邊界被觸碰時的回調(diào) |
onViewCaptured(View capturedChild, int activePointerId) | 當視圖被捕獲時的回調(diào) |
onViewDragStateChanged(int state) | 當視圖的拖動狀態(tài)改變的時候的回調(diào) |
onViewPositionChanged(View changedView, int left, int top, int dx, int dy) | 當捕獲的視圖位置發(fā)生改變的時候的回調(diào) |
onViewReleased(View releasedChild, float xvel, float yvel) | 當視圖的拖動被釋放的時候的回調(diào) |
tryCaptureView(View child, int pointerId) | 判斷此時的視圖是否為想要捕獲的視圖時會調(diào)用 |
getOrderedChildIndex(int index) | 獲取子視圖的Z值 |
//方法的大致的回調(diào)順序:
1)shouldInterceptTouchEvent:
DOWN:
getOrderedChildIndex(findTopChildUnder)
->onEdgeTouched
MOVE:
getOrderedChildIndex(findTopChildUnder)
->getViewHorizontalDragRange &
getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
->clampViewPositionHorizontal&
clampViewPositionVertical
->onEdgeDragStarted
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
2)processTouchEvent:
DOWN:
getOrderedChildIndex(findTopChildUnder)
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
->onEdgeTouched
MOVE:
->STATE==DRAGGING:dragTo
->STATE!=DRAGGING:
onEdgeDragStarted
->getOrderedChildIndex(findTopChildUnder)
->getViewHorizontalDragRange&
getViewVerticalDragRange(checkTouchSlop)
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
例子
1)任意移動
2)移動完畢后回到原位
3)邊界移動時對View進行捕獲(未成功。。)
public class VDHDemo extends LinearLayout {
private static final String TAG = "VDHDemo";
private ViewDragHelper mDragger;
private View mDragView;
private View mAutoBackView;
private Point mAutoBackOriPos = new Point();
public VDHDemo(Context context, AttributeSet attrs) {
super(context, attrs);
//第二個參數(shù)為敏感度(sensitivity),敏感度越大mTouchSlop就越小
//mTouchSlop為系統(tǒng)認為是移動的最小距離,即ViewConfiguration.get(context).getScaledPagingTouchSlop()
mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//返回true表示可以捕獲該view,可根據(jù)第一個參數(shù)決定捕獲哪個view
//如: return xxView == child;
return mDragView==child || mAutoBackView==child;
// return true;
}
//邊界控制
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final int leftBound = getPaddingLeft(); //左邊界為viewgroup的paddingleft
final int rightBound = getWidth() - leftBound - getPaddingRight() - 200; //200為子view的寬度
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
//邊界控制
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
//手指釋放時回調(diào)
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// super.onViewReleased(releasedChild, xvel, yvel);
//若為mAutoBackView,則回到初始位置,調(diào)用settleCapturedViewAt()
//其內(nèi)部為mScroller.startScroll(),別忘了invalidate和computeScroll
//注意你拖動的越快,返回的越快
if (releasedChild == mAutoBackView){
mDragger.settleCapturedViewAt(mAutoBackOriPos.x,mAutoBackOriPos.y);
invalidate();
}
}
//如果子View不消耗事件堪滨,那么整個手勢(DOWN-MOVE*-UP)都是直接進入onTouchEvent发笔,
// 在onTouchEvent的DOWN的時候就確定了captureView
//如果消耗事件,那么就會先走onInterceptTouchEvent方法舅锄,判斷是否可以捕獲,
// 而在判斷的過程中會去判斷另外兩個回調(diào)的方法:getViewHorizontalDragRange和getViewVerticalDragRange司忱,
// 只有這兩個方法返回大于0的值才能正常的捕獲皇忿。
@Override
public int getViewHorizontalDragRange(View child)
{
return getMeasuredWidth()-child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child)
{
return getMeasuredHeight()-child.getMeasuredHeight();
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragger.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mDragView = getChildAt(0);
mAutoBackView = getChildAt(1);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//onLayout結(jié)束后將mAutoBackView的返回原點設(shè)置為其初始的點
mAutoBackOriPos.x = mAutoBackView.getLeft();
mAutoBackOriPos.y = mAutoBackView.getTop();
}
@Override
public void computeScroll() {
if (mDragger.continueSettling(true)){
invalidate();
}
}
}