1. 程式人生 > >Android實現滑動的七種方法實踐

Android實現滑動的七種方法實踐

  在講解滑動之前,要先熟悉一下安卓的座標系。安卓檢視有兩個座標系,一個是Android座標系,一個是檢視座標系。前者以螢幕的最左上角為原點,向右為X軸正方向,向下為Y軸正方向。後者以父檢視的左上角為原點,其它與前者一致。

  而獲取座標的方法也可以分為兩類,View提供的獲得座標的方法和MotionEvent提高的方法。View提供的方法有getTop(),getLeft(),getBottom(),getRight(),而MotionEvent提供的方法有getX(),getY(),getRawX(),getRawY()。

  如上圖所示,getLeft()方法為View自身左邊到父佈局左邊的距離,getTop()方法為View自身的頂邊到父佈局頂邊的距離,getRight()為View自身的右邊到父佈局左邊的距離,getBottom()為View自身底邊到父佈局頂邊的距離。

  而getX()為為觸控點到View左邊的距離,getY()為觸控點到View頂邊的距離。getRawX()為觸控事件到螢幕左邊的距離,getRawY()為觸控事件到螢幕頂邊的距離。

  下面進入正題:

方法一:layout方法

注意:layout方法的引數是(getLeft()+offsetX,getTop()+offsetY.getRight()+offsetX,getBottom()+offsetY),offsetX為水平方向偏移量,offsetY為豎直方向偏移量

關於偏移量的計算可以採用getX()和getRawX()兩種方法

首先看使用getX()的方法

</pre><p></p><pre name="code" class="java">import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class DragView1 extends View {

    private int lastX;
    private int lastY;

    public DragView1(Context context) {
        super(context);
        ininView();
    }

    public DragView1(Context context, AttributeSet attrs) {
        super(context, attrs);
        ininView();
    }

    public DragView1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ininView();
    }

    private void ininView() {
        // 給View設定背景顏色,便於觀察
        setBackgroundColor(Color.BLUE);
    }

    // 檢視座標方式
    @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;
                // 在當前left、top、right、bottom的基礎上加上偏移量
                layout(getLeft() + offsetX,
                        getTop() + offsetY,
                        getRight() + offsetX,
                        getBottom() + offsetY);
//                        offsetLeftAndRight(offsetX);
//                        offsetTopAndBottom(offsetY);
                break;
        }
        return true;
    }
}


如果將上面程式碼

int x = (int) event.getX();

int y = (int) event.getY();

直接改成getRawX()和getRawY()的話,就會發現圖形移動的幅度遠大於觸控點移動的幅度(以我的經驗,圖形很容易就飛出螢幕),而解決這個問題的方法如下。

</pre><pre name="code" class="java">@Override
    public boolean onTouchEvent(MotionEvent event) {
        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);
                // 重新設定初始座標
                <strong>lastX = rawX;
                lastY = rawY;</strong>
                break;
        }
        return true;
    }


也就是在重寫onTouchEvent 方法時加上加粗的程式碼,目的是重置初始座標。不過為什麼要這麼做呢?

以下是我在除錯過程中獲得的資料,首先是使用getX()方法時獲得的資料(排版有些問題,大家見諒)

08-16 02:01:15.493 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X217Y148offsetX0offsetY2

08-16 02:01:15.551 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y146offsetX4offsetY0

08-16 02:01:15.690 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X220Y149offsetX3offsetY3
08-16 02:01:15.740 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y146offsetX4offsetY0
08-16 02:01:15.778 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y150offsetX4offsetY4
08-16 02:01:15.823 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X226Y153offsetX9offsetY7
08-16 02:01:15.861 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X215Y151offsetX-2offsetY5
08-16 02:01:15.910 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y147offsetX4offsetY1
08-16 02:01:15.924 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X219Y152offsetX2offsetY6
08-16 02:01:15.962 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X218Y149offsetX1offsetY3
08-16 02:01:15.975 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y154offsetX4offsetY8
08-16 02:01:16.041 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X224Y156offsetX7offsetY10
08-16 02:01:16.079 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X217Y155offsetX0offsetY9
08-16 02:01:16.111 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y154offsetX4offsetY8
08-16 02:01:16.124 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X222Y145offsetX5offsetY-1
08-16 02:01:16.177 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X216Y151offsetX-1offsetY5
08-16 02:01:16.242 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X217Y148offsetX0offsetY2
08-16 02:01:16.273 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y152offsetX4offsetY6
08-16 02:01:16.311 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X220Y151offsetX3offsetY5
08-16 02:01:16.346 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X217Y151offsetX0offsetY5
08-16 02:01:16.400 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X217Y150offsetX0offsetY4
08-16 02:01:16.472 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X227Y152offsetX10offsetY6
08-16 02:01:16.507 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X217Y150offsetX0offsetY4
08-16 02:01:16.522 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X218Y157offsetX1offsetY11
08-16 02:01:16.544 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X217Y154offsetX0offsetY8
08-16 02:01:16.578 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y150offsetX4offsetY4
08-16 02:01:16.608 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X222Y145offsetX5offsetY-1
08-16 02:01:16.627 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X216Y151offsetX-1offsetY5
08-16 02:01:16.667 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X217Y150offsetX0offsetY4
08-16 02:01:16.700 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X222Y152offsetX5offsetY6
08-16 02:01:16.750 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X223Y154offsetX6offsetY8
08-16 02:01:16.793 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X221Y154offsetX4offsetY8
08-16 02:01:16.822 10322-10322/com.example.tai.dragviewtest D/DEBUG: lastX217lastY146X222Y145offsetX5offsetY-1

可以很清楚地看到,由於在觸控點移動時相應的控制元件也跟著移動,所以效果類似於自動充值,X,Y偏移量都處於一個正常的範圍內。

下面看一下getRawX()和getRawY()的情況

08-16 01:28:58.550 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X269Y278

08-16 01:28:58.562 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X284Y307

08-16 01:28:58.611 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X412Y485
08-16 01:28:58.642 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X466Y561
08-16 01:28:58.669 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X495Y595
08-16 01:28:58.777 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X574Y703
08-16 01:28:58.778 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X581Y706
08-16 01:28:58.797 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X603Y747
08-16 01:28:58.828 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X607Y751
08-16 01:28:58.839 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X622Y777
08-16 01:28:58.906 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X626Y781

08-16 01:28:58.924 10322-10322/com.example.tai.dragviewtest D/Debug/t: LastX250LastY237X629Y781

雖然沒有列印offsetX和offsetY的值,但可以清楚的看到,由於沒有座標重置,導致offsetX和offsetY的實際值遠大於本身的偏移量,因此導致控制元件唯一幅度遠大於觸控點位移的幅度。至於解決方法就是上面所說的。

方法二 offsetLeftAndRight()和offsetTopAndBottom

offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
直接用以上程式碼來替代layout方法,原理是一樣的。

方法三 LayoutParams

我覺得原理是和以上兩種一樣的,只不過換一下寫法,下面是程式碼。

import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

public class DragView3 extends View{
    int lastX;
    int lastY;
    public DragView3(Context context)
    {
        super(context);
        initView();
    }
    public DragView3(Context context,AttributeSet attributeSet)
    {
        super(context,attributeSet);
        initView();
    }
    public DragView3(Context context,AttributeSet attributeSet,int defStyleAttr)
    {
        super(context,attributeSet,defStyleAttr);
        initView();
    }
    private void initView()
    {
        setBackgroundColor(Color.BLUE);
    }
    @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;
                LinearLayout.LayoutParams layoutParams=(LinearLayout.LayoutParams)getLayoutParams();
                layoutParams.topMargin=getTop()+offsetY;
                layoutParams.leftMargin=getLeft()+offsetX;
                setLayoutParams(layoutParams);
                break;

        }
        return true;

    }
}
關於
LinearLayout.LayoutParams layoutParams=(LinearLayout.LayoutParams)getLayoutParams();
你的父佈局是什麼,這個“LinearLayout”就寫什麼(前提是有父佈局)

此外還可以這麼寫

 ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
效果是一樣的。

方法四 scrollTo 和scrollBy

這兩個方法的區別很明顯,一個是To,是到某位置;一個是By,是在原有基礎上加上一個偏移量,先說後者。

原理依舊是計算楚偏移量,具體的改變如下。

 ((View) getParent()).scrollBy(-offsetX, -offsetY);
只有這麼一點改變,至於為什麼是負的,這就涉及檢視移動的原理了,簡單地說一下。

可以這麼想象,所有的檢視都畫在一張畫布上,而上面蓋了一塊木板,木板上有螢幕那麼大的一個空缺,我們看到的實際上是透過木板看到的畫布上的景象,你看不到,不代表畫布上沒畫。而scroll兩個方法其實都是在移動木板而不是畫布,因此方向是相反的(可以自己比劃一下)。

然後是scrollTo方法,要注意的是該方法需要用getRawX()和getRawY()獲得座標,另外別忘了重置座標,否則會出現之前的問題。

((View)getParent()).scrollTo(-x,-y);

方法五:Scroller
這個方法和方法四極為相似,不過通過該方法可以讓滑動有一個過程(而不是瞬間從一個地方消失再出現另一個地方),這樣可以使滑動效果不那麼突兀。

使用這個方法的重點在於重寫computeScroll()方法(系統在繪製View時會在onDraw()方法中呼叫此方法)

public class DragView5 extends View {
    int lastX;
    int lastY;
    Scroller mscroller;
    public DragView5(Context context)
    {
        super(context);
        initView(context);
    }
    public DragView5(Context context, AttributeSet attributeSet)
    {
        super(context,attributeSet);
        initView(context);
    }
    public DragView5(Context context,AttributeSet attributeSet,int defStyleAttr)
    {
        super(context,attributeSet,defStyleAttr);
        initView(context);
    }

    private void initView(Context context)
    {
        setBackgroundColor(Color.BLUE);
        mscroller=new Scroller(context);
    }
    @Override
    public void computeScroll(){
        super.computeScroll();
        if(mscroller.computeScrollOffset())
        {
            ((View)getParent()).scrollTo(mscroller.getCurrX(),mscroller.getCurrY());
            invalidate();
        }
    }
    @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;
                ((View)getParent()).scrollBy(-offsetX,-offsetY);
                break;
            case MotionEvent.ACTION_UP:
                View viewGroup=(View)getParent();
               mscroller.startScroll(viewGroup.getScrollX(),viewGroup.getScrollY(),-viewGroup.getScrollX(),-viewGroup.getScrollY());
                invalidate();
                break;
        }
        return true;
    }
}
方法六 屬性動畫

方法七 ViewDragHelper

這個方法我並不熟悉,能看懂,但自己用就很吃力了,也就不多說了,上程式碼(這個的程式碼實現了類似於QQ側邊欄效果)

import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

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();
        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 void initView() {
        mViewDragHelper = ViewDragHelper.create(this, callback);
    }

    private ViewDragHelper.Callback callback =
            new ViewDragHelper.Callback() {

                // 何時開始檢測觸控事件
                @Override
                public boolean tryCaptureView(View child, int pointerId) {
                    //如果當前觸控的child是mMainView時開始檢測
                    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);
                }

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

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

                // 拖動結束後呼叫
                @Override
                public void onViewReleased(View releasedChild, float xvel, float yvel) {
                    super.onViewReleased(releasedChild, xvel, yvel);
                    //手指擡起後緩慢移動到指定位置
                    if (mMainView.getLeft() < 500) {
                        //關閉選單
                        //相當於Scroller的startScroll方法
                        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);
        }
    }
}


這是我第一次寫部落格,這篇部落格是我在學習《Android群英錄》的時候寫的總結,本來算是一個私人的筆記,但我覺得可能對同時初學者的其他人有用,所以最後還是發了出來。

這些程式碼基本上都是我從《Android群英錄》個github上弄下來的,自己看明白後把原來的註釋掉自己寫一遍,然後碰到了一些問題和解決辦法記了下來。

這篇部落格除了程式碼都是純手打,我個人覺得勉強算是原創,如果我弄錯了,請聯絡我,我會及時刪除。