Android Scroll 分析

鏈接 Android Scroll 分析

這是我重讀《Android 群英傳》的時候做的讀書筆記,這里主要講了 Android 坐標系和視圖坐標系,以及實現滑動的多種實現方法。

Android 坐標系和視圖坐標系

區別

  • Android 坐標系:左上角作為原點,由 getLocationScreen(int location[]) 獲取點的位置,或在觸控事件中使用 getRawX()、getRawY()獲得Android 坐標系中的坐標。

  • 視圖坐標系:子視圖在父視圖中的位置關系,同樣,父視圖的左上角為坐標原點,通過 getX()getY() 獲得視圖坐標系的坐標。

獲取坐標值

View 提供的獲取坐標方法

getTop()getLeft()、getRight()getBottiom() 獲取到的是 View 自身的頂邊、左邊、右邊、底邊到其父布局頂邊、左邊、右邊、底邊的距離。

MotionEvent 提供的方法

  • getX(), getY() 獲取到點擊事件距離控件左邊、頂邊的距離,即視圖坐標
  • getRawX(), getRawY() 獲取點擊事件距離整個屏幕左邊、頂邊的距離,即絕對坐標

實現滑動的方法

滑動的思想:觸摸 View 時,系統記下當前觸摸點坐標;當手指移動時,系統記下移動后的觸摸點坐標,從而獲取到相對于前一次坐標點的偏移量,并通過偏移量修改 View 的坐標。如此重復,從而實現滑動的過程。

1. layout 方法

@Override
public boolean onTouchEvent(MotionEvent event){
    //絕對坐標,當然也可以通過 getX() 視圖坐標獲取偏移量, 兩種方式得到的偏移量都是相同的
    //但是注意,使用絕對坐標系,一定要在每次執行完 ACTION_MOVE 的邏輯后,重設初始坐標,才能準確地獲取偏移量
    int rawX = (int)event.getRawX();
    int rawY = (int)event.getRawY();
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            //記錄觸摸點坐標
            lastX = rawX;
            lastY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            //計算偏移量
            int offsetX = rawX - lastX;
            int offsetY = rawY - lastY;
            //在當前 left, top, right, bottom 的基礎上加上偏移量
            layout(getLeft() + offsetX,
                getTop() + offsetY,
                getRight() + offsetX,
                getBottom() + offsetY);
            //重設初始坐標
            lastX = rawX;
            lastY = rawY;
            break;
    }
    return true;
}

2. offsetLeftAndRight()offsetTopAndBottom() 方法

與 layout 方法 效果相同,只是多了一層封裝而已

//同時對 left 和 right 進行偏移
offsetLeftAndRight(offsetX);
//同時對 top 和 bottom 進行偏移
offsetTopAndBottom(offsetY);

3. LayoutParams

LayoutParams 保存了一個 View 的布局參數,所以通過改變 LayoutParams 來動態修改一個布局的位置參數,從而達到改變 View 位置的效果。

LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

所以,其實我們改變的是這個 View 的 Margin 屬性

4. scrollTo 和 scrollBy

  • scrollTo(x, y) 移動到一個具體的坐標點

  • scrollBy(dx, dy) 移動的增量為 dx, dy

scrollTo 和 scrollBy 移動的是 View 的內容。即:對 TextView 使用的話,則是移動它的文本;對 ViewGroup 使用的話,則移動的是所有的子 View。所以,一般不對 View 使用這兩個方法,而是對 ViewGroup 使用。

int offsetX = x - lastX;
int offsetY = y - lastY;
//注:隨手指移動的話,偏移量要為負
((View)getParent()).scrollBy(-offsetX, -offsetY);

5. Scroller

Scroller 類可以實現平滑移動的效果,而不再是瞬間完成的移動。

Scroller 原理:和 scrollTo, scrollBy 類似,只是,在 ACTION_MOVE 中不斷獲取手指移動的微小偏移量,將一段距離劃分為 N 個非常小的偏移量。在每個偏移量里面通過 scrollBy 方法進行瞬間移動,實現平滑移動。

例:讓子 View 跟隨手指滑動,但在手指離開屏幕時,讓子 View 平滑移動到初始位置,即屏幕左上角。

使用 Scroller 類需要如下三個步驟:

a. 初始化 Scroller
mScroller = new Scroller(context);
b. 重寫 computeScroll() 方法,實現模擬滾動
//模板代碼
//系統在繪制 View 的時候會在 draw() 方法中調用該方法
//該方法實際上就是使用 scrollTo 方法, 不斷的瞬間移動一個小距離實現整體的平滑移動效果
@Override
public void computeScroll(){
    super.computeScroll();
    //判斷 Scroller 是否執行完畢
    if(mScroller.computeScrollOffset()){
        ((View)getParent()).scrollTo(
            mScroller.getCurrX(),//獲得當前的滑動坐標
            mScroller.getCurrY());
        //通過重繪不斷調用 computeScroll
        invalidate();
    }
}

需要注意:因為只能在 computeScroll() 方法中獲取模擬過程的 scrollX 和 scrollY 坐標,但 computeScroll() 不會自動調用,只能通過 invalidate() -> draw() -> computeScroll() 來間接調用 computeScroll(),所以需要在上述代碼中調用invalidate(),實現循環獲取 scrollX 和 scrollY 的目的。模擬過程結束后,scroller.computeScrollOfset() 方法會返回 false, 從而中斷循環,完成整個平滑移動的過程。

c. startScroll 開啟模擬過程
case MotionEvent.ACTION_UP:
    //手指離開時,執行滑動過程,讓子 View 平滑移動到初始位置,即屏幕左上角
    View viewGroup = ((View)getParent());
    mScroller.startScroll(
        viewGroup.getScrollX(),//起始坐標
        viewGroup.getScrollY(),
        -viewGroup.getScrollX(),//偏移量
        -viewGroup.getScrollY());
    //通知重繪!
    invalidate();
    break;

6. 屬性動畫

略(我也等筆記做到第7章的動畫機制的時候再寫吧,哈哈)

7. ViewDragHelper

例:實現 QQ 滑動側邊欄的布局

public class DragViewGroup extends FrameLayout {

    private ViewDragHelper mViewDragHelper;
    private View mMenuView, mMainView;
    private int mWidth;

    public DragViewGroup(Context context) {
        super(context);
        initView();
    }

    public DragViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public DragViewGroup(Context context,
                         AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    //加載布局文件完成后調用
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //按順序將子 View 分別定義成 MenuView 和 MainView
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 獲取 View 的寬度
        // 如果需要根據 View 的寬度來處理滑動后的效果,可以使用這個值來判斷
        mWidth = mMenuView.getMeasuredWidth();
    }

    //步驟二:攔截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //將事件傳遞給 ViewDragHelper 處理
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //將觸摸事件傳遞給ViewDragHelper,此操作必不可少
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    //步驟一:初始化
    private void initView() {
        //使用靜態工廠方法初始化
        //參數1 通常是一個 ViewGroup,即parentView
        //參數2 是一個 Callback 回調,是整個 ViewDragHelper 的邏輯核心
        mViewDragHelper = ViewDragHelper.create(this, callback);
    }

    //步驟四:處理回調 Callback
    private ViewDragHelper.Callback callback =
            new ViewDragHelper.Callback() {

                // 何時開始檢測觸摸事件
                // 通過該方法,指定在創建 ViewDragHelper 時,參數 parentView 中的哪一個子 View 可以被移動
                @Override
                public boolean tryCaptureView(View child, int pointerId) {
                    //如果當前觸摸的child是mMainView時開始檢測
                    //即 只有 MainView 可以被拖動
                    return mMainView == child;
                }

                // 觸摸到View后回調
                @Override
                public void onViewCaptured(View capturedChild,
                                           int activePointerId) {
                    super.onViewCaptured(capturedChild, activePointerId);
                }

                // 當拖拽狀態改變,比如idle,dragging
                @Override
                public void onViewDragStateChanged(int state) {
                    super.onViewDragStateChanged(state);
                }

                // 當位置改變的時候調用,常用與滑動時更改scale等
                @Override
                public void onViewPositionChanged(View changedView,
                                                  int left, int top, int dx, int dy) {
                    super.onViewPositionChanged(changedView, left, top, dx, dy);
                }

                // 處理垂直滑動
                // top: 垂直方向上 child 移動的距離
                // dy: 相較前一次的增量
                @Override
                public int clampViewPositionVertical(View child, int top, int dy) {
                    return 0;//垂直方向上不發生滑動
                }

                // 處理水平滑動
                @Override
                public int clampViewPositionHorizontal(View child, int left, int dx) {
                    return left;
                }

                // 拖動結束后調用
                // 即手指離開屏幕后實現的操作
                // 該方法內部是通過 Scroller 類來實現的
                @Override
                public void onViewReleased(View releasedChild, float xvel, float yvel) {
                    super.onViewReleased(releasedChild, xvel, yvel);
                    //手指抬起后緩慢移動到指定位置
                    //讓 MainView 移動后左邊距小于500像素時,就使用 smoothSlideViewTo() 將 MainView 還原到初始狀態,即(0,0)的點
                    if (mMainView.getLeft() < 500) {
                        //關閉菜單
                        //相當于Scroller的startScroll方法
                        mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                    } else {
                        //打開菜單
                        //大于500時,移動到(300, 0)坐標,即顯示 MenuView
                        mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                    }
                }
            };

    //步驟三:處理 computeScroll()
    //因為 ViewDragHelper 內部同樣是通過 Scroller 來實現平滑移動的,所以重寫該方法
    //可作為模板代碼
    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}

ViewDragHelper.Callback 中其他的一些強大的事件

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

推薦閱讀更多精彩內容

  • 前言 本篇談論Android Scroll的應用以及如何在應用中添加滑動效果。你可以學到: 發生滑動效果的原因 如...
    張文靖同學閱讀 577評論 0 1
  • 概念 滑動是如何產生的 滑動一個VIew,本質上是移動一個View。移動一個View需要改變他的坐標,所以滑動一個...
    Reiser實驗室閱讀 295評論 0 0
  • 什么是View View 是 Android 中所有控件的基類。 View的位置參數 View 的位置由它的四個頂...
    acc8226閱讀 1,202評論 0 7
  • 導語 滑動算是Android比較常用的效果了,滑動的操作具有很好的用戶體驗性。 主要內容 滑動效果是如何產生的 實...
    一個有故事的程序員閱讀 6,471評論 3 11
  • 頌圣這件事,本來就是咱們中國“歷史悠久的傳統”。 久到多久呢。大概起頭是《詩經》。 《商頌玄鳥》 天命玄鳥,降而生...
    石京學閱讀 3,179評論 0 2