1. 程式人生 > >[學習筆記] Android群英傳:Android Scroll分析

[學習筆記] Android群英傳:Android Scroll分析

一.滑動效果的產生

1.Android座標系

在Android,系統將螢幕最左上角的頂點作為Android座標系的原點,從這個點向右是X軸正方向,從這個點向下是Y軸正方向,如圖

這裡寫圖片描述

系統提供了getLocationOnScreen(intlocation[])來獲取Android座標中的位置,即該檢視左上角Android的座標,另外,在觸控事件中使用getRawX(),getRawY()方法來獲取Android座標系中的座標。

2.檢視座標系

檢視座標系,描述了子檢視在父檢視的位置關係,檢視座標系同樣的以原點向右為X正方向,以原點向下為Y方法,在檢視座標系中,原點不再是Android座標系中的螢幕左上角,而是以父檢視左上角為座標原點

這裡寫圖片描述

在觸控事件中通過getX,getY來獲取的座標就是檢視座標中的座標

3.觸控事件——MotionEvent

看MotionEvent中封裝了一些常量,定義了觸控事件的不同型別。

    //單點觸控按下的動作
    public static final int ACTION_DOWN = 0;
    //單點觸控離開的動作
    public static final int ACTION_UP = 1;
    //單點觸控移動的動作
    public static final int ACTION_MOVE = 2;
    //單點觸控取消
    public static
final int ACTION_CANCEL = 3; //單點觸控超出邊界 public static final int ACTION_OUTSIDE = 4; //多點觸控按下的動作 public static final int ACTION_POINTER_DOWN = 5; //多點觸控離開的動作 public static final int ACTION_POINTER_UP = 6;

通常情況下,我們會在onTouchEvent(MotionEvent event)方法中通過event.getAction()來獲取觸控事件的型別,並使用switch來判斷

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //獲取當前輸入點的X,Y座標(檢視座標)
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //處理輸入的按下動作
                break;
            case MotionEvent.ACTION_MOVE:
                //處理輸入的移動動作
                break;
            case MotionEvent.ACTION_UP:
                //處理輸入的離開動作
                break;
        }
        return true;
    }

下圖總結一些常用的API:

這裡寫圖片描述

這些方法可以分成兩個類別

View提供的獲取座標方法

getTop():獲取到的是View自身的頂部到其父佈局頂部的距離

getLeft():獲取到的是View自身的左邊到其父佈局左邊的距離

getRight():獲取到的是View自身的右邊到其父佈局右邊的距離

getBottom():獲取到的是View自身的底部到其父佈局底部的距離

MotionEvent提供的方法

getX():獲取點選事件距離控制元件左邊的距離,即檢視座標

getY():獲取點選事件距離控制元件頂部的距離,即檢視座標

getRawX:獲取點選事件整個螢幕左邊的距離,即絕對座標

getRawY:獲取點選事件整個螢幕頂部的距離,即絕對座標

二.實現滑動的七種方法

通過例項來看看Android中如何實現滑動的效果:
定義一個View,簡單的實現一個佈局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.zza.demo.DragView
        android:layout_width="100dp"
        android:layout_height="100dp" />
</RelativeLayout>

預設的顯示:

這裡寫圖片描述

1.layout方法

在View的繪製上,會呼叫onLayout()方法來設定顯示的位置,同樣可以修改View的left,top,right,bottom四個屬性來控制View的座標

 //觸控事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();
        int lastX = 0;
        int lastY = 0;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //記錄觸控點的座標
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                //計算偏移量
                int officeX = rawX - lastX;
                int officeY = rawY - lastY;
                //在當前的left,top,right,bottom基礎上加上偏移量
                layout(getLeft()+officeX,getTop()+officeY,getRight()+officeX,getBottom()+officeY);
                //重新設定初始值
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_UP:
                //處理輸入的離開動作
                break;
        }
        return true;
    }

2.offsetLeftAndRight()與offsetTopAndBottom()

系統提供的一個對左右,上下移動的封裝,當計算出偏移量的時候,只需要使用如下的程式碼就可以完成View的重新佈局,效果和使用Layout()方法是一樣的

//同時對左右偏移
offsetLeftAndRight(officeX);
//同時對上下偏移
offsetTopAndBottom(officeY);

3.LayoutParams

LayoutParams保留了一個View的佈局引數,因此可以在程式中,通過改變LayoutParams來動態改變一個佈局的位置引數,從而改變View位置的效果,我們可以很方便的在程式中使用getLayoutParams()來獲取一個View的LayoutParams,當然,在計算偏移量的方法和Layout方法中計算offset是一樣的,當獲取到偏移量之後,可以通過setLayoutParams來改變LayoutParams:

LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft()+officeX;
                layoutParams.topMargin = getTop()+officeY;
                setLayoutParams(layoutParams);

注意,通過getLayoutParams()獲取layoutParams時,需要根據View所在的跟佈局的型別來設定不同的型別,比如View放在LinearLayout裡那就是LinearLayout.LayoutParams,比如在RelativeLayout裡就是 RelativeLayout.LayoutParams,不然系統是無法獲取到layoutParams的.

在通過一個layoutParams來改變一個View的位置時,通常改變的是這個view的Margin屬性,所以除了使用佈局的layoutParams屬性外,還需要 ViewGroup.MarginLayoutParams來實現這樣的功能

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft()+officeX;
                layoutParams.topMargin = getTop()+officeY;
                setLayoutParams(layoutParams);

用ViewGroup不用去管父佈局是什麼

4.scrollTo與scrollBy

在一個View當中,系統提供了scrollTo與scrollBy這兩種方式來實現移動一個View的位置
scrollTo(x,y);表示移動到一個具體的點
scrollBy(dx,dy);表示移動的增量

int officeX = rawX - lastX;
int officeY = rawY - lastY;
scrollBy(officeX,officeY);

但是,當拖動View的時候,View並沒有移動,只是移動了view的content,如果用ViewGroup使用to和by的話,那所有的子View都將移動,要是在View中使用的話,那麼移動的就是View的內容了

在該View所在的ViewGroup中使用scrollBy方法來移動這個view

  ((View)getParent()).scrollBy(officeX,officeY);

但是,拖動View的時候,View雖然移動了,並不是我們想要的跟隨觸控點的移動而移動

當呼叫scrollBy的方法時,可以想象外面的ViewGroup在移動,具體的例子

這裡寫圖片描述

上圖,中間的矩形相當於螢幕,即可視區域,後面的content相當於畫布,代表檢視,只有檢視的中間部分目前是可視的,其他部分都不可見,可見區域中設定一個button,他的座標是(20.10),下面我們使用scrollBy(20.10)方法來進行移動,如圖:

這裡寫圖片描述

設定scrollBy(20.10),偏移量均為XY的正方向,但是螢幕的可視區域,Button卻向反方向移動了。
參考系選擇的不同,產生不同效果。

我們將scrollBy的引數dx,dy設定成正數,那麼content將向座標軸負方向移動,反之,則正方向

int officeX = rawX - lastX;
int officeY = rawY - lastY;
scrollBy(-officeX,-officeY);

5.Scroller

Scroller就可以實現平滑的效果

  • 初始化scroller
    首先,通過他的構造方法來建立一個scroller物件
//初始化mScroller
mScroller = new Scroller(context);
  • 重寫computeScroll,實現模擬滑動
    computeScroll這個方法,是使用Scroller的核心,系統在繪製View的同時,會在onDraw()方法中呼叫這個方法
    通常情況下,computeScroll的程式碼可以利用標準的寫法:
    /**
     * 模擬滑動
     */
    @Override
    public void computeScroll() {
        super.computeScroll();

        //判斷Scroller是否執行完畢
        if(mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        }
        //通過重繪來不斷呼叫computeScroll
        invalidate();
    }

Scroller類提供了computeScrollOffset()來判斷是否完成了整個頁面的滑動,提供了getCurrX(),getCurrY()來獲取當前滑動座標。

注意invalidate(),只能在computeScroll中獲得模擬過程中的scrollX,scrollY座標,但computeScroll方法是不會自動呼叫的,只能通過invalidate——>OnDraw——>computeScroll來呼叫,所以需要這個invalidate,而當模擬過程結束的時候,computeScrollOffset返回的是false,從而結束迴圈

  • startScroll開啟模擬過程
    使用平滑移動事件,使用Scroller類的startScroll()方法來開啟平滑過程,startScroll有兩個過載的方法
    • public void startScroll(int startX,int startY,int dx,int dy)
    • public void startScroll(int startX,int startY,int dx)

例項,在構造分鐘初始化Scroller物件,然後重寫computeScroll方法,最後需要監聽手指離開螢幕的事件,並在該事件之後呼叫startScroll()完成平移,所以我們在ACTION_UP中

case MotionEvent.ACTION_UP:
                //處理輸入的離開動作
                View view = ((View)getParent());
                mScroller.startScroll(view.getScrollX(),view.getScrollY(),-view.getScrollX(),-view.getScrollY());
                invalidate();
                break;

6.ViewDragHelper

Google在其support庫中為我們提供了一個DrawerLayout和SlidingPaneLayout兩個佈局來幫助開發者實現側滑效果,這兩個佈局背後,隱藏著一個ViewDragHelper,通過ViewDragHelper,基本可以實現各種不同的側滑,拖放需求

使用ViewDragHelper實現一個QQ滑動側邊欄的佈局

  • 初始化ViewDragHelper

首先是初始化ViewDragHelper,ViewDragHelper通常定義在一個ViewGroup中,通過其靜態方法初始化

 mViewDragHelper = ViewDragHelper.create(this,callback);

他的第一個引數是要監聽的View,第二個引數是一個Callback回撥,這個回撥是整個業務的核心,

  • 攔截事件

要重寫攔截事件,將事件傳遞給ViewDragHelper進行處理

 //事件攔截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    //觸控事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //將觸控事件傳遞給ViewDragHelper

        mViewDragHelper.processTouchEvent(event);

        return true;
    }
  • 處理computeScroll()

使用ViewDragHelper也是需要重寫computeScroll的,因為ViewDragHelper內部也是通過Scroller來實現平移的,我們可以這樣使用

    @Override
    public void computeScroll() {
        if(mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
  • 處理回撥Cakkback
//側滑回調
    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        //何時開始觸控
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            //如果當前觸控的child是mMainView開始檢測
            return mMainView == child;
        }

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

        //處理垂直滑動
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }

        //拖動結束後呼叫
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //手指擡起後緩慢的移動到指定位置
            if(mMainView.getLeft() <500){
                //關閉選單
                mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }else{
                //開啟選單
                mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }
    };

下面自定義一個viewGroup來完成整個編碼的例項

/**
 * 側滑
 */
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();
    }

    //初始化資料
    private void initView() {
        mViewDragHelper = ViewDragHelper.create(this,callback);
    }

    //XML載入組建後回撥
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }


    //元件大小改變時回撥
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenuView.getMeasuredWidth();
    }

    //事件攔截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    //觸控事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //將觸控事件傳遞給ViewDragHelper

        mViewDragHelper.processTouchEvent(event);

        return true;
    }

    //側滑回調
    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        //何時開始觸控
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            //如果當前觸控的child是mMainView開始檢測
            return mMainView == child;
        }

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

        //處理垂直滑動
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }

        //拖動結束後呼叫
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //手指擡起後緩慢的移動到指定位置
            if(mMainView.getLeft() <500){
                //關閉選單
                mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }else{
                //開啟選單
                mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }
    };

    @Override
    public void computeScroll() {
        if(mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}

在Cakkback中系統提供了很多的方法來監聽

  • onViewCaptured
        //使用者觸控到view回撥
        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
        }
  • onViewDragStateChanged
        //拖拽狀態改變時,比如idle,dragging
        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
        }
  • onViewPositionChanged
//位置發生改變,常用語滑動scale效果
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }