Android Scroll分析

參考資料

郭霖 Scroller完全解析
鴻洋 ViewDragHelper完全解析
鴻洋 ViewDragHelper實(shí)戰(zhàn) 自己打造Drawerlayout


-目錄

  • 1)layout
  • 2)offsetLeftAndRight() offsetTopAndBottom()
  • 3)LayoutParams()
  • 4)scrollTo() scrollBy()
  • 5)Scroller
  • 6)屬性動(dòng)畫(huà)
  • 7)ViewDragHelper

-實(shí)現(xiàn)滑動(dòng)的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()

任何一個(gè)控件都是可以滾動(dòng)的,因?yàn)閂iew類(lèi)中有scrollTo()和scrollBy()兩個(gè)方法,scrollBy()是讓View相對(duì)于當(dāng)前位置滾動(dòng)某段距離,scrollTo()是讓View相對(duì)于初始位置滾動(dòng)某段距離。

scrollTo,scrollBy方法移動(dòng)的是View的內(nèi)容,如果ViewGroup中使用scrollTo,scrollBy,那么移動(dòng)的將是所有子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),按鈕確是向手機(jī)坐標(biāo)系的x和y軸正向移動(dòng)呢?
答:可以想象屏幕是一個(gè)放大鏡,而下面是一個(gè)巨大的畫(huà)布,使用scrollBy方法,將layout向X軸負(fù)方向(左)平移60,向Y軸負(fù)方向(上)平移100,則layout內(nèi)的子view相當(dāng)于向X軸和Y軸的正方向上移動(dòng)了。

20160110164232041.gif

5) Scroller

使用Scroller模仿ViewPager的例子

startScroll(int startX,int startY,int dx, int dy,int duration)
startScroll(int startX,int startY,int dx, int dy)
20160114230048304.gif
/**
 * 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);
        //大于這個(gè)距離,系統(tǒng)認(rèn)為是移動(dò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:
                //按下的坐標(biāo)與當(dāng)前移動(dòng)坐標(biāo)絕對(duì)值 大于 系統(tǒng)默認(rèn)的移動(dòng)距離
                //攔截此移動(dòng)事件,不向子view傳遞,進(jìn)入自身的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之類(lèi)的clickable控件,則會(huì)由button消費(fèi)掉down事件,當(dāng)viewgroup滑動(dòng)時(shí),會(huì)攔截move事件并處理
                //但是若子控件為T(mén)extView之類(lèi)的非clickable控件,則viewgroup和textview都不會(huì)消費(fèi)掉down事件.
                //由于沒(méi)有任何view消費(fèi)down事件,后續(xù)事件將由上層消費(fèi),而不會(huì)往下傳遞給viewgroup.所以此處需要將down事件消費(fèi)掉,從而能繼續(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;
                }
                //滑動(dòng)處理
                scrollBy(offsetX,0);
                mMoveX = x;
                break;
            case MotionEvent.ACTION_UP:
                //手指抬起,判斷是哪個(gè)子控件的index
                //小于第一個(gè)子控件的一半寬度則認(rèn)為是第一個(gè)子控件
                //大于第一個(gè)子控件的一半寬度則認(rèn)為是下一個(gè)子控件
                int index = (getScrollX()+getWidth()/2)/getWidth();
                Log.i(TAG, "index: "+index); //結(jié)果為  0  1  2
                //根據(jù)子空間index計(jì)算偏移量
                int dy = index * getWidth() - getScrollX();
                Log.i(TAG, "dy: "+dy);
                mScroller.startScroll(getScrollX(),0,dy,0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    //重繪會(huì)調(diào)用此方法,此方法中的invalidate又會(huì)觸發(fā)重繪,從而循環(huán)實(shí)現(xiàn)彈性滑動(dòng)
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }
}

6) 屬性動(dòng)畫(huà)(動(dòng)畫(huà)中講解)


7) ViewDragHelper

在自定義ViewGroup中,很多效果都包含用戶(hù)手指去拖動(dòng)其內(nèi)部的某個(gè)View(eg:側(cè)滑菜單等),針對(duì)具體的需要去寫(xiě)好onInterceptTouchEvent和onTouchEvent這兩個(gè)方法是一件很不容易的事,需要自己去處理:多手指的處理、加速度檢測(cè)等等。
好在官方在v4的支持包中提供了ViewDragHelper這樣一個(gè)類(lèi)來(lái)幫助我們方便的編寫(xiě)自定義ViewGroup

1)ViewDragHelper類(lèi)相關(guān)的API:

方法 說(shuō)明
create(ViewGroup forParent, ViewDragHelper.Callback cb) 創(chuàng)建viewDragHelper
captureChildView(View childView, int activePointerId) 捕獲子視圖
checkTouchSlop(int directions, int pointerId) 檢查移動(dòng)是否為最小的滑動(dòng)速度
findTopChildUnder(int x, int y) 返回指定位置上的頂部子視圖
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) 解決捕獲視圖自由滑動(dòng)的位置
getActivePointerId() 獲取活動(dòng)的子視圖的id
getCapturedView() 獲取捕獲的視圖
getEdgeSize() 獲取邊界的大小
getMinVelocity() 獲取最小的速度
getTouchSlop() 獲取最小的滑動(dòng)速度
getViewDragState() 獲取視圖的拖動(dòng)狀態(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) 滑動(dòng)側(cè)邊欄到指定的位置
shouldInterceptTouchEvent(MotionEvent ev) 處理父容器是否攔截事件
processTouchEvent(MotionEvent ev) 處理父容器攔截的事件

2)ViewDragHelper.Callback相關(guān)API:

方法 說(shuō)明
clampViewPositionHorizontal(View child, int left, int dx) 控制橫軸的移動(dòng)距離
clampViewPositionVertical(View child, int top, int dy) 控制縱軸的移動(dòng)距離
getViewHorizontalDragRange(View child) 獲取視圖在橫軸移動(dòng)的距離
getViewVerticalDragRange(View child) 獲取視圖在縱軸的移動(dòng)距離
onEdgeDragStarted(int edgeFlags, int pointerId) 處理當(dāng)用戶(hù)觸碰邊界移動(dòng)開(kāi)始的回調(diào)
onEdgeLock(int edgeFlags) 處理邊界被鎖定時(shí)的回調(diào)
onEdgeTouched(int edgeFlags, int pointerId) 處理邊界被觸碰時(shí)的回調(diào)
onViewCaptured(View capturedChild, int activePointerId) 當(dāng)視圖被捕獲時(shí)的回調(diào)
onViewDragStateChanged(int state) 當(dāng)視圖的拖動(dòng)狀態(tài)改變的時(shí)候的回調(diào)
onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 當(dāng)捕獲的視圖位置發(fā)生改變的時(shí)候的回調(diào)
onViewReleased(View releasedChild, float xvel, float yvel) 當(dāng)視圖的拖動(dòng)被釋放的時(shí)候的回調(diào)
tryCaptureView(View child, int pointerId) 判斷此時(shí)的視圖是否為想要捕獲的視圖時(shí)會(huì)調(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)任意移動(dòng)
2)移動(dòng)完畢后回到原位
3)邊界移動(dòng)時(shí)對(duì)View進(jìn)行捕獲(未成功。。)

20150713095339390.gif
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);
        //第二個(gè)參數(shù)為敏感度(sensitivity),敏感度越大mTouchSlop就越小
        //mTouchSlop為系統(tǒng)認(rèn)為是移動(dò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ù)第一個(gè)參數(shù)決定捕獲哪個(gè)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;
            }

            //手指釋放時(shí)回調(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
                //注意你拖動(dòng)的越快,返回的越快
                if (releasedChild == mAutoBackView){
                    mDragger.settleCapturedViewAt(mAutoBackOriPos.x,mAutoBackOriPos.y);
                    invalidate();
                }
            }
            //如果子View不消耗事件,那么整個(gè)手勢(shì)(DOWN-MOVE*-UP)都是直接進(jìn)入onTouchEvent,
            // 在onTouchEvent的DOWN的時(shí)候就確定了captureView

            //如果消耗事件,那么就會(huì)先走onInterceptTouchEvent方法,判斷是否可以捕獲,
            // 而在判斷的過(guò)程中會(huì)去判斷另外兩個(gè)回調(diào)的方法:getViewHorizontalDragRange和getViewVerticalDragRange,
            // 只有這兩個(gè)方法返回大于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的返回原點(diǎn)設(shè)置為其初始的點(diǎn)
        mAutoBackOriPos.x = mAutoBackView.getLeft();
        mAutoBackOriPos.y = mAutoBackView.getTop();
    }

    @Override
    public void computeScroll() {
        if (mDragger.continueSettling(true)){
            invalidate();
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 本篇談?wù)揂ndroid Scroll的應(yīng)用以及如何在應(yīng)用中添加滑動(dòng)效果。你可以學(xué)到: 發(fā)生滑動(dòng)效果的原因 如...
    張文靖同學(xué)閱讀 630評(píng)論 0 1
  • 鏈接 Android Scroll 分析 這是我重讀《Android 群英傳》的時(shí)候做的讀書(shū)筆記,這里主要講了 A...
    MrFu閱讀 1,206評(píng)論 4 28
  • 內(nèi)容是博主照著書(shū)敲出來(lái)的,博主碼字挺辛苦的,轉(zhuǎn)載請(qǐng)注明出處,后序內(nèi)容陸續(xù)會(huì)碼出。 當(dāng)了解了Android坐標(biāo)系和觸...
    Blankj閱讀 6,861評(píng)論 3 60
  • 概念 滑動(dòng)是如何產(chǎn)生的 滑動(dòng)一個(gè)VIew,本質(zhì)上是移動(dòng)一個(gè)View。移動(dòng)一個(gè)View需要改變他的坐標(biāo),所以滑動(dòng)一個(gè)...
    Reiser實(shí)驗(yàn)室閱讀 356評(píng)論 0 0
  • 大家知道有一本書(shū)名字就叫孤獨(dú)是生命的禮物。這個(gè)書(shū)名太貼近我心了,我享受孤獨(dú)帶給我的慰藉也享受著它的純真。沒(méi)錯(cuò),孤...
    錢(qián)満満閱讀 287評(píng)論 0 1

友情鏈接更多精彩內(nèi)容