Android 常見 View 控件 滑動 和 拖動的幾種方式

(1)使用scrollTo/scrollBy實現View的滑動

實現滑動的最樸素直接的方式就是使用View類自帶的scrollTo/scrollBy方法了。
能夠直接linearLayout.getScaleY(); 和 查看 mScrollX 變量,源碼裏面看註釋就能夠發現:
mScrollX:表示離視圖起始位置的x水平方向的偏移量
mScrollY:表示離視圖起始位置的y垂直方向的偏移量
分別經過getScrollX() 和getScrollY()方法得到。
注意:mScrollX和mScrollY指的並非座標,而是偏移量。java

public void scrollTo(int x, int y) {  
      if (mScrollX != x || mScrollY != y) {  
          int oldX = mScrollX;  
          int oldY = mScrollY;  
          mScrollX = x;  
          mScrollY = y;  
          onScrollChanged(mScrollX, mScrollY, oldX, oldY);  
          if (!awakenScrollBars()) {  
              invalidate();  
          }  
      }  
}  

public void scrollBy(int x, int y) {  
      scrollTo(mScrollX + x, mScrollY + y);  
}

從以上的代碼能夠看出,scrollTo 和 scrollBy區別,其實2者的效果是同樣的。android

scrollTo(int x,int y):
若是偏移位置發生了改變,就會給mScrollX和mScrollY賦新值,改變當前位置。
注意:x,y表明的不是座標點,而是偏移量。
例如:
我要移動view到座標點(100,100),那麼個人偏移量就是(0,,0) -(100,100) = (-100,-100),我就要執行view.scrollTo(-100,-100),達到這個效果。ide

scrollBy(int x,int y):
從源碼中看出,它其實是調用了scrollTo(mScrollX + x, mScrollY + y);mScrollX + x和mScrollY + y,即表示在原先偏移的基礎上在發生偏移,通俗的說就是相對咱們當前位置偏移。佈局

scrollTo方法是滑動到指定位置,而scrollBy方法是滑動指定的位移量,在原先的基礎上發生偏移。
根據父類VIEW裏面移動,若是移動到了超出的地方,就不會顯示。post

代碼以下 :動畫

咱們瞭解到使用scrollTo/scrollBy方法實現View的滑動是很簡單直接的,那麼簡單的背後有什麼代價呢?代價就是滑動不是「彈性的」,能夠用個定時器不停的調用,讓它開起來是順滑的。this

 

(2)使用 動畫 實現View的滑動

使用動畫來實現View的滑動主要經過改變View的translationX和translationY參數來實現,使用動畫的好處在於滑動效果是平滑的。
上面咱們提到過,View的x、y參數決定View的當前位置,經過改變translationX和translationY,咱們就能夠改變View的當前位置。
咱們可使用屬性動畫或者補間動畫來實現View的平移。
使用動畫的實現方式比較簡單,下面就使用間補動畫和屬性動畫作個案例,以下 : spa

首先,咱們先來看一下如何使用補間動畫來實現View的平移。補間動畫資源定義以下(anim.xml):code

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100"/>
</set>

而後在onCreat方法中調用startAnimation方法便可。使用補間動畫實現View的滑動有一個缺陷,那就是移動的知識View的「影像」,這意味着其實View並未真正的移動,只是咱們看起來它移動了而已。拿Button來舉例,倘若咱們經過補間動畫移動了一個Button,咱們會發現,在Button的原來位置點擊屏幕會出發點擊事件,而在移動後的Button上點擊不會觸發點擊事件。xml


接下來,咱們看看如何用屬性動畫來實現View的平移。使用屬性動畫實現View的平移更加簡單,只須要如下一條語句:

button.setOnClickListener((v) -> {
      float translationX = (int) v.getTranslationX();
     ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(v, "translationX", translationX, -100, translationX);
     objectAnimator.setDuration(3000);
     objectAnimator.start();
});


以上代碼即實現了使用屬性動畫把targetView在3秒內向右平移100px。使用屬性動畫的限制在於真正的屬性動畫只能夠在Android 3.0+使用(一些第三方庫實現的兼容低版本的屬性動畫不是真正的屬性動畫),優勢就是它能夠真正的移動View而不是僅僅移動View的影像。
通過以上的描述,使用屬性動畫實現View的滑動看起來是個不錯的選擇,並且一些View的複雜的滑動效果只有經過動畫才能比較方便的實現。

屬性動畫實現效果以下 : (其實很順滑的,只是錄製效果有點差。)

 

(3)使用 LayoutParams 實現View的滑動


經過改變佈局參數來實現View的滑動的思想很簡單:
好比向右移動一個View,只須要把它的marginLeft參數增大,向其它方向移動同理,只需改變相應的margin參數。
還有一種比較拐彎抹角的方法是在要移動的View的旁邊預先放一個View(初始寬高設爲0)。
而後好比咱們要向右移動View,只需把預先放置的那個View的寬度增大,這樣就把View「擠」到右邊了。代碼示例以下:

LinearLayout.LayoutParams lps = (LinearLayout.LayoutParams) v.getLayoutParams();
lps.leftMargin += 100;
v.requestLayout();

以上代碼即實現了把mButton向右滑動100px。經過改變佈局參數來實現的滑動效果也不是平滑的。

 

(4)使用Scroller來實現彈性滑動


Android裏Scroller類是爲了實現View平滑滾動的一個Helper類。一般在自定義的View時使用,在View中定義一個私有成員mScroller = new Scroller(context)。設置mScroller滾動的位置時,並不會致使View的滾動,一般是用mScroller記錄/計算View滾動的位置,再重寫View的computeScroll(),完成實際的滾動。 相關API介紹以下 
 

mScroller.getCurrX() //獲取mScroller當前水平滾動的位置  
mScroller.getCurrY() //獲取mScroller當前豎直滾動的位置  
mScroller.getFinalX() //獲取mScroller最終中止的水平位置  
mScroller.getFinalY() //獲取mScroller最終中止的豎直位置  
mScroller.setFinalX(int newX) //設置mScroller最終停留的水平位置,沒有動畫效果,直接跳到目標位置  
mScroller.setFinalY(int newY) //設置mScroller最終停留的豎直位置,沒有動畫效果,直接跳到目標位置  
  
//滾動,startX, startY爲開始滾動的位置,dx,dy爲滾動的偏移量, duration爲完成滾動的時間  
mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默認完成時間250ms  
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)  
mScroller.computeScrollOffset() //返回值爲boolean,true說明滾動還沒有完成,false說明滾動已經完成。這是一個很重要的方法,一般放在View.computeScroll()中,用來判斷是否滾動是否結束。  


舉例說明,自定義一個CustomView,使用Scroller實現滾動: 

import android.content.Context;  
import android.util.AttributeSet;  
import android.util.Log;  
import android.view.View;  
import android.widget.LinearLayout;  
import android.widget.Scroller;  
  
public class CustomView extends LinearLayout {  
  
    private static final String TAG = "Scroller";  
    private Scroller mScroller;  
  
    public CustomView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mScroller = new Scroller(context);  
    }  
  
    //調用此方法滾動到目標位置  
    public void smoothScrollTo(int fx, int fy) {  
        int dx = fx - mScroller.getFinalX();  
        int dy = fy - mScroller.getFinalY();  
        smoothScrollBy(dx, dy);  
    }  
  
    //調用此方法設置滾動的相對偏移  
    public void smoothScrollBy(int dx, int dy) {  
        //設置mScroller的滾動偏移量  
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);  
        invalidate();//這裏必須調用invalidate()才能保證computeScroll()會被調用,不然不必定會刷新界面,看不到滾動效果  
    }  
      
    @Override  
    public void computeScroll() {  
        //先判斷mScroller滾動是否完成  
        if (mScroller.computeScrollOffset()) {  
            //這裏調用View的scrollTo()完成實際的滾動  
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
            //必須調用該方法,不然不必定能看到滾動效果  
            postInvalidate();  
        }  
        super.computeScroll();  
    }  
}  

效果圖以下 : 

(5)使用ViewDragHelper來實現View拖動

ViewDragHelper從名稱上就能夠看出, 這是一個用來簡化view拖拽操做的幫助類。並且使用起來也很簡單, 很方便,只須要幾個方法和1個Callback就能夠實現一個能夠拖動到view。
須要注意個是:ViewDragHelper是做用在一個ViewGroup上,也就是說他不能直接做用到被拖拽的view, 其實這也很好理解,由於view在佈局中的位置是父ViewGroup決定的。
如何使用ViewGroup實現一個能夠拖動的view?

一、獲取ViewDragHelper的實例,注意,這裏不能直接new,而是使用ViewDragHelper的一個靜態方法:
ViewDragHelper.create(ViewGroup forParent, float sensitivity, ViewDragHelper.Callback cb);
參數1: 一個ViewGroup, 也就是ViewDragHelper將要用來拖拽誰下面的子view
參數2:靈敏度,通常設置爲1.0f就行
參數3:一個回調,用來處理拖動到位置
二、繼承ViewDragHelper.Callback類,該類有個抽象方法:tryCaptureView(View view, int pointerId) 表示嘗試捕獲子view,這裏必定要返回true, 返回true表示容許。
三、重寫兩個方法int clampViewPositionHorizontal(View child, int left, int dx)和int clampViewPositionHorizontal(View child, int left, int dx) 這兩個方法分別用來處理x方向和y方向的拖動的,返回值該child如今的位置。
四、重寫ViewGroup的onInterceptTouchEvent(MotionEvent ev)用來攔截事件
五、重寫ViewGroup的onTouchEvent(MotionEvent event) 在這裏面只要作兩件事:mDragHelper.processTouchEvent(event);處理攔截到的事件,這個方法會在返回前分發事件;return true 表示消費了事件。

簡單的5步就能夠實現一個能夠任意拖動到view了,固然咱們須要在clampViewPositionHorizontal和clampViewPositionHorizontal中作一點工做,以防止view超出了邊界。

下面咱們本身定義一個CustomViews來實現能夠隨意拖動的View

public class CustomViews extends LinearLayout {
    private ViewDragHelper mDragHelper;

    public CustomViews(Context context) {
        super(context);
        init();
    }

    public CustomViews(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomViews(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        /**
         * @params ViewGroup forParent 必須是一個ViewGroup
         * @params float sensitivity 靈敏度
         * @params Callback cb 回調
         */
        mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
    }

    private class ViewDragCallback extends ViewDragHelper.Callback {
        /**
         * 嘗試捕獲子view,必定要返回true
         *
         * @param view      嘗試捕獲的view
         * @param pointerId 指示器id?
         *                  這裏能夠決定哪一個子view能夠拖動
         */
        @Override
        public boolean tryCaptureView(View view, int pointerId) {
            return true;
        }

        /**
         * 處理水平方向上的拖動
         *
         * @param child 被拖動到view
         * @param left  移動到達的x軸的距離
         * @param dx    建議的移動的x距離
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            System.out.println("left = " + left + ", dx = " + dx);
            // 兩個if主要是爲了讓viewViewGroup裏
            if (getPaddingLeft() > left) {
                return getPaddingLeft();
            }
            if (getWidth() - child.getWidth() < left) {
                return getWidth() - child.getWidth();
            }
            return left;
        }

        /**
         * 處理豎直方向上的拖動
         *
         * @param child 被拖動到view
         * @param top   移動到達的y軸的距離
         * @param dy    建議的移動的y距離
         */
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            // 兩個if主要是爲了讓viewViewGroup裏
            if (getPaddingTop() > top) {
                return getPaddingTop();
            }
            if (getHeight() - child.getHeight() < top) {
                return getHeight() - child.getHeight();
            }
            return top;
        }

        /**
         * 當拖拽到狀態改變時回調
         *
         * @params 新的狀態
         */
        @Override
        public void onViewDragStateChanged(int state) {
            switch (state) {
                case ViewDragHelper.STATE_DRAGGING:  // 正在被拖動
                    break;
                case ViewDragHelper.STATE_IDLE:  // view沒有被拖拽或者 正在進行fling/snap
                    break;
                case ViewDragHelper.STATE_SETTLING: // fling完畢後被放置到一個位置
                    break;
            }
            super.onViewDragStateChanged(state);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_DOWN:
                mDragHelper.cancel(); // 至關於調用 processTouchEvent收到ACTION_CANCEL
                break;
        }
        /**
         * 檢查是否能夠攔截touch事件
         * 若是onInterceptTouchEvent能夠return true 則這裏return true
         */
        return mDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        /**
         * 處理攔截到的事件
         * 這個方法會在返回前分發事件
         */
        mDragHelper.processTouchEvent(event);
        return true;
    }

}

XML 使用以下 : 

    <com.and.mvp.base.ganhuoi.module.test.CustomViews
        android:id="@+id/customViews"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="移動我" />
    </com.and.mvp.base.ganhuoi.module.test.CustomViews>

效果圖 以下 :

 (6)offsetLeftAndRight() offsetTopAndBottom()

其實這兩個方法分別是對左右移動和上下移動的封裝,傳入的就是偏移量。

下面就寫一個案例,Java代碼以下 : 

public class MyText extends TextView {

    int lastX;
    int lastY;

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

    @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 offX = x - lastX;
                int offY = y - lastY;
                offsetLeftAndRight(offX);
                offsetTopAndBottom(offY);
                break;
        }
        return true;//記得返回true,說明被咱們這裏消化了改事件
    }

}

XML 使用方法以下 : 

      <com.and.mvp.base.ganhuoi.module.test.MyText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="移動我" />

效果圖以下 : 

(7) layout()

若是你將滑動後的目標位置的座標傳遞給layout(),這樣子就會把view的位置給從新佈置了一下,在視覺上就是view的一個滑動的效果。

這個方法和 第6個是同樣的 只不過最後調用的方法不同罷了

    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 offX = x - lastX;
                int offY = y - lastY;
                //調用layout方法來從新放置它的位置  ,其餘的和第6個方法是同樣的,只是下面調用的這行代碼不同的罷了。
                layout(getLeft() + offX, getTop() + offY,
                        getRight() + offX, getBottom() + offY);
                break;
        }
        return true;
    }
相關文章
相關標籤/搜索