Android Scroll分析

參考資料

郭霖 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軸的正方向上移動了橘霎。

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);
        //大于這個距離,系統(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進行捕獲(未成功。。)

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);
        //第二個參數(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();
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市坦仍,隨后出現(xiàn)的幾起案子鳍烁,更是在濱河造成了極大的恐慌,老刑警劉巖繁扎,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幔荒,死亡現(xiàn)場離奇詭異,居然都是意外死亡梳玫,警方通過查閱死者的電腦和手機爹梁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來提澎,“玉大人姚垃,你說我怎么就攤上這事∨渭桑” “怎么了积糯?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長碴犬。 經(jīng)常有香客問我絮宁,道長,這世上最難降的妖魔是什么服协? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任绍昂,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘窘游。我一直安慰自己唠椭,他們只是感情好,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布忍饰。 她就那樣靜靜地躺著贪嫂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪艾蓝。 梳的紋絲不亂的頭發(fā)上力崇,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機與錄音赢织,去河邊找鬼亮靴。 笑死,一個胖子當著我的面吹牛于置,可吹牛的內(nèi)容都是我干的茧吊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼八毯,長吁一口氣:“原來是場噩夢啊……” “哼搓侄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起话速,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤讶踪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后尿孔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俊柔,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年活合,在試婚紗的時候發(fā)現(xiàn)自己被綠了雏婶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡白指,死狀恐怖留晚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情告嘲,我是刑警寧澤错维,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站橄唬,受9級特大地震影響赋焕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜仰楚,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一隆判、第九天 我趴在偏房一處隱蔽的房頂上張望犬庇。 院中可真熱鬧,春花似錦侨嘀、人聲如沸臭挽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽欢峰。三九已至,卻和暖如春涨共,著一層夾襖步出監(jiān)牢的瞬間纽帖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工举反, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抛计,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓照筑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瘦陈。 傳聞我的和親對象是個殘疾皇子凝危,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 前言 本篇談?wù)揂ndroid Scroll的應(yīng)用以及如何在應(yīng)用中添加滑動效果。你可以學到: 發(fā)生滑動效果的原因 如...
    張文靖同學閱讀 569評論 0 1
  • 鏈接 Android Scroll 分析 這是我重讀《Android 群英傳》的時候做的讀書筆記晨逝,這里主要講了 A...
    MrFu閱讀 1,148評論 4 28
  • 內(nèi)容是博主照著書敲出來的蛾默,博主碼字挺辛苦的,轉(zhuǎn)載請注明出處捉貌,后序內(nèi)容陸續(xù)會碼出支鸡。 當了解了Android坐標系和觸...
    Blankj閱讀 6,641評論 3 61
  • 概念 滑動是如何產(chǎn)生的 滑動一個VIew,本質(zhì)上是移動一個View趁窃。移動一個View需要改變他的坐標牧挣,所以滑動一個...
    Reiser實驗室閱讀 291評論 0 0
  • 大家知道有一本書名字就叫孤獨是生命的禮物。這個書名太貼近我心了醒陆,我享受孤獨帶給我的慰藉也享受著它的純真瀑构。沒錯,孤...
    錢満満閱讀 247評論 0 1