Android Scroll分析

參考資料

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


-目錄

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

-實現滑動的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的內容,如果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內的子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); //結果為  0  1  2
                //根據子空間index計算偏移量
                int dy = index * getWidth() - getScrollX();
                Log.i(TAG, "dy: "+dy);
                mScroller.startScroll(getScrollX(),0,dy,0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

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

6) 屬性動畫(動畫中講解)


7) ViewDragHelper

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

1)ViewDragHelper類相關的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) 設置邊界跟蹤
settleCapturedViewAt(int finalLeft, int finalTop) 設置捕獲的視圖到指定的位置
smoothSlideViewTo(View child, int finalLeft, int finalTop) 滑動側邊欄到指定的位置
shouldInterceptTouchEvent(MotionEvent ev) 處理父容器是否攔截事件
processTouchEvent(MotionEvent ev) 處理父容器攔截的事件

2)ViewDragHelper.Callback相關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) 處理當用戶觸碰邊界移動開始的回調
onEdgeLock(int edgeFlags) 處理邊界被鎖定時的回調
onEdgeTouched(int edgeFlags, int pointerId) 處理邊界被觸碰時的回調
onViewCaptured(View capturedChild, int activePointerId) 當視圖被捕獲時的回調
onViewDragStateChanged(int state) 當視圖的拖動狀態(tài)改變的時候的回調
onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 當捕獲的視圖位置發(fā)生改變的時候的回調
onViewReleased(View releasedChild, float xvel, float yvel) 當視圖的拖動被釋放的時候的回調
tryCaptureView(View child, int pointerId) 判斷此時的視圖是否為想要捕獲的視圖時會調用
getOrderedChildIndex(int index) 獲取子視圖的Z值
//方法的大致的回調順序:

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);
        //第二個參數為敏感度(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,可根據第一個參數決定捕獲哪個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;
            }

            //手指釋放時回調
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
//                super.onViewReleased(releasedChild, xvel, yvel);
                //若為mAutoBackView,則回到初始位置,調用settleCapturedViewAt()
                //其內部為mScroller.startScroll(),別忘了invalidate和computeScroll
                //注意你拖動的越快,返回的越快
                if (releasedChild == mAutoBackView){
                    mDragger.settleCapturedViewAt(mAutoBackOriPos.x,mAutoBackOriPos.y);
                    invalidate();
                }
            }
            //如果子View不消耗事件,那么整個手勢(DOWN-MOVE*-UP)都是直接進入onTouchEvent,
            // 在onTouchEvent的DOWN的時候就確定了captureView

            //如果消耗事件,那么就會先走onInterceptTouchEvent方法,判斷是否可以捕獲,
            // 而在判斷的過程中會去判斷另外兩個回調的方法: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結束后將mAutoBackView的返回原點設置為其初始的點
        mAutoBackOriPos.x = mAutoBackView.getLeft();
        mAutoBackOriPos.y = mAutoBackView.getTop();
    }

    @Override
    public void computeScroll() {
        if (mDragger.continueSettling(true)){
            invalidate();
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容

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