ViewDragHelper: ViewDragHelper的使用


一. 背景知識

2013年谷歌i/o大會上介紹了兩個新的layout: SlidingPaneLayoutDrawerLayout,現在這倆個類被廣泛的運用,其實研究他們的源碼你會發現這兩個類都運用了ViewDragHelper來處理拖動。ViewDragHelper是framework中不為人知卻非常有用的一個工具。

ViewDragHelper解決了android中手勢處理過於復雜的問題,在DrawerLayout出現之前,側滑菜單都是由第三方開源代碼實現的,其中著名的當屬 MenuDrawer ,MenuDrawer重寫onTouchEvent方法來實現側滑效果,代碼量很大,實現邏輯也需要很大的耐心才能看懂。如果每個開發人員都從這么原始的步奏開始做起,那對於安卓生態是相當不利的。所以說ViewDragHelper等的出現反映了安卓開發框架已經開始向成熟的方向邁進。

二. 解決的問題

ViewDragHelper is a utility class for writing custom ViewGroups. 
It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.

從官方注釋可以看出:ViewDragHelper主要可以用來拖拽和設置ViewGroup中子View的位置。

三. 可以實現的效果

1. View的拖動效果

摘自:https://blog.csdn.net/lmj623565791/article/details/46858663

2. 仿微信語音通知的懸浮窗效果(懸浮窗可以拖動,並且在手指釋放的時候,懸浮窗會自動停靠在屏幕邊緣)

摘自:https://www.jianshu.com/p/a9e0a98e4d42

3. 抽屜效果

摘自:https://cloud.tencent.com/developer/article/1035828

四. 基本使用

1. ViewDragHelper的初始化

ViewDragHelper一般用在一個自定義ViewGroup的內部,比如下面自定義了一個繼承於LinearLayout的DragLayout,DragLayout內部有一個子view mDragView作為成員變量:

public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragHelper;

    private View mDragView;

    public DragLayout(Context context) {
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

}

創建一個帶有回調接口的 ViewDragHelper 

public DragLayout(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}

其中1.0f是靈敏度系數,系數越大越敏感。第一個參數為this,表示要拖動子View所在的Parent View,必須為ViewGroup

要讓ViewDragHelper能夠處理拖動,還需要將觸摸事件傳遞給ViewDragHelper,這點和 Gesturedetector 是一樣的:

 @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
    return mDragHelper.shouldInterceptTouchEvent(event);
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
    mDragHelper.processTouchEvent(event);
    return true;
 }

接下來,你就可以在剛才傳入的回調中處理各種拖動行為了。

2. 拖動行為的處理

處理橫向的拖動:

在DragHelperCallback中實現 clampViewPositionHorizontal 方法, 並且返回一個適當的數值就能實現橫向拖動效果,clampViewPositionHorizontal的第二個參數是指當前拖動子view應該到達的x坐標。所以按照常理這個方法原封返回第二個參數就可以了,但為了讓被拖動的view遇到邊界之后就不在拖動,對返回的值做了更多的考慮。

@Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
            // 最小x坐標值不能小於leftBound
            final int leftBound = getPaddingLeft();
            // 最大x坐標值不能大於rightBound
            final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight();
            final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
            return newLeft;
        }

同上,處理縱向的拖動:

在DragHelperCallback中實現clampViewPositionVertical方法,實現過程同 clampViewPositionHorizontal 

@Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            Log.d("DragLayout", "top=" + top + "; dy=" + dy);
            // 最小 y 坐標值不能小於 topBound
            final int topBound = getPaddingTop();
            // 最大 y 坐標值不能大於 bottomBound
            final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom();
            final int newTop = Math.min(Math.max(top, topBound), bottomBound);
            return newTop;
        }

 

clampViewPositionHorizontal 和 clampViewPositionVertical必須要重寫,因為默認它返回的是0。事實上我們在這兩個方法中所能做的事情很有限。 個人覺得這兩個方法的作用就是給了我們重新定義目的坐標的機會。

完整code 參考:

activity_test_view.xml

<?xml version="1.0" encoding="utf-8"?>
<com.yongdaimi.android.androidapitest.view.DragLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".ListViewActivity">

    <TextView
        android:id="@+id/content_view"
        android:layout_width="100dip"
        android:layout_height="100dip"
        android:background="@android:color/holo_blue_light"
        android:text="內容區域"
        android:gravity="center"
        />

</com.yongdaimi.android.androidapitest.view.DragLayout>
View Code

DragLayout.java

package com.yongdaimi.android.androidapitest.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;

import androidx.annotation.NonNull;
import androidx.customview.widget.ViewDragHelper;

public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragHelper;

    private View mDragView;

    public DragLayout(Context context) {
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
    }


    private class DragHelperCallback extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
            return true;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
            // 最小x坐標值不能小於leftBound
            final int leftBound = getPaddingLeft();
            // 最大x坐標值不能大於rightBound
            final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight();
            final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
            return newLeft;
        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            Log.d("DragLayout", "top=" + top + "; dy=" + dy);
            // 最小 y 坐標值不能小於 topBound
            final int topBound = getPaddingTop();
            // 最大 y 坐標值不能大於 bottomBound
            final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom();
            final int newTop = Math.min(Math.max(top, topBound), bottomBound);
            return newTop;
        }
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mDragHelper.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragHelper.processTouchEvent(event);
        return true;
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mDragView = getChildAt(0);
    }

}
View Code

五. ViewDragHelper.Callback的一些常用API的分析

我們注意到在創建ViewDragHelper時,需要傳入一個Callback, 目前我們分別使用了這個Callback的 clampViewPositionHorizontal  和  clampViewPositionVertical 方法,用於實現對被拖動View的邊界進行控制,實際上這個Callback大概有13個左右的方法,下面對一些常用的Callback 方法做下分析:

1. tryCaptureView(@NonNull View child, int pointerId)

這是一個抽象方法,也是Callback類中唯一 一個沒有實現的抽象方法,在實例化ViewDragHelper.Callback時會強制要求我們去實現。

這個方法的作用是用於控制哪些View可以被捕獲,默認值返回true, 則代表可以捕獲ViewGroup中的所有View, 也就是所有的View均可以被拖動,如果只想讓指定的View被拖動,則可以將指定View的返回值設置成true。例:

修改 DragLayout.java, 重寫Callback下的 tryCaptureView 方法:

public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragHelper;

    private View mDragView;
    private View mNotDragView;

    public DragLayout(Context context) {
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
    }


    private class DragHelperCallback extends ViewDragHelper.Callback {

        @Override public boolean tryCaptureView(@NonNull View child, int pointerId) {
            // 設置只有mDragView 可以被拖動
            return child == mDragView; }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
            // 最小x坐標值不能小於leftBound
            final int leftBound = getPaddingLeft();
            // 最大x坐標值不能大於rightBound
            final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight();
            final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
            return newLeft;
        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            Log.d("DragLayout", "top=" + top + "; dy=" + dy);
            // 最小 y 坐標值不能小於 topBound
            final int topBound = getPaddingTop();
            // 最大 y 坐標值不能大於 bottomBound
            final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom();
            final int newTop = Math.min(Math.max(top, topBound), bottomBound);
            return newTop;
        }
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mDragHelper.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragHelper.processTouchEvent(event);
        return true;
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mDragView = getChildAt(0);
        mNotDragView = getChildAt(1);
    }

}

同時在對應的XML文件中新增一個View:

<?xml version="1.0" encoding="utf-8"?>
<com.yongdaimi.android.androidapitest.view.DragLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".ListViewActivity">

    <TextView
        android:layout_width="100dip"
        android:layout_height="100dip"
        android:background="@android:color/holo_blue_light"
        android:text="內容區域"
        android:gravity="center"
        />


    <TextView
        android:layout_width="100dip"
        android:layout_height="100dip"
        android:background="@android:color/holo_green_light"
        android:text="內容區域1"
        android:gravity="center"
        />

</com.yongdaimi.android.androidapitest.view.DragLayout>

再次運行,發現只有第一個藍色的方塊可以滑動了。

2. onViewReleased(@NonNull View releasedChild, float xvel, float yvel)

這個方法會在View停止拖拽的時候調用,也就是手指釋放的時候調用,利用這個方法可以實現回彈效果:修改DragLayout.java,重寫onViewReleased方法:

package com.yongdaimi.android.androidapitest.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;

import androidx.annotation.NonNull;
import androidx.customview.widget.ViewDragHelper;

public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragHelper;

    private View mDragView;
    private View mNotDragView;

    private View mBounceView;


    private int mCurrentTop;
    private int mCurrentLeft;

    public DragLayout(Context context) {
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
    }


    private class DragHelperCallback extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
            // 設置只有mDragView 可以被拖動
            return child != mNotDragView;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
            // 最小x坐標值不能小於leftBound
            final int leftBound = getPaddingLeft();
            // 最大x坐標值不能大於rightBound
            final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight();
            final int newLeft = Math.min(Math.max(left, leftBound), rightBound);

            mCurrentLeft = newLeft;
            return newLeft;
        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            Log.d("DragLayout", "top=" + top + "; dy=" + dy);
            // 最小 y 坐標值不能小於 topBound
            final int topBound = getPaddingTop();
            // 最大 y 坐標值不能大於 bottomBound
            final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom();
            final int newTop = Math.min(Math.max(top, topBound), bottomBound);

            mCurrentTop = newTop;
            return newTop;
        }

        @Override public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (releasedChild == mBounceView) {
                int childWidth = releasedChild.getWidth();
                int parentWidth = getWidth();
                int leftBound = getPaddingLeft(); // 左邊界
                int rightBound = parentWidth - releasedChild.getWidth() - getPaddingRight(); // 右邊界

                // 如果方塊的中點超過 ViewGroup 的中點時,滑動到左邊緣,否則滑動到右邊緣
                if ((childWidth / 2 + mCurrentLeft) < parentWidth / 2) {
                    mDragHelper.settleCapturedViewAt(leftBound, mCurrentTop);
                } else { mDragHelper.settleCapturedViewAt(rightBound, mCurrentTop); } invalidate(); } }
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mDragHelper.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragHelper.processTouchEvent(event);
        return true;
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mDragView = getChildAt(0);
        mNotDragView = getChildAt(1);
        mBounceView = getChildAt(2);
    }

    @Override public void computeScroll() {
        super.computeScroll();
        if (mDragHelper != null && mDragHelper.continueSettling(true)) { invalidate(); } }

}

修改XML

<?xml version="1.0" encoding="utf-8"?>
<com.yongdaimi.android.androidapitest.view.DragLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".ListViewActivity">

    <TextView
        android:layout_width="100dip"
        android:layout_height="100dip"
        android:background="@android:color/holo_blue_light"
        android:text="內容區域"
        android:gravity="center"
        />


    <TextView
        android:layout_width="100dip"
        android:layout_height="100dip"
        android:background="@android:color/holo_green_light"
        android:text="內容區域1"
        android:gravity="center"
        />

    <TextView
        android:layout_width="100dip"
        android:layout_height="100dip"
        android:background="@android:color/holo_orange_light"
        android:text="內容區域2"
        android:gravity="center"
        />

</com.yongdaimi.android.androidapitest.view.DragLayout>

運行:發現當橙色滑塊在超過中軸線的位置時會自動停靠在邊緣。

這里調用了 settleCapturedViewAt 方法來重新設置被捕獲的View的位置,同時調用了invalidate()來要求界面進行重繪,但僅靠這個還不足,注意這個方法的注釋:

If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
on each subsequent frame to continue the motion until it returns false. If this method
returns false there is no further work to do to complete the movement.

所以還需要在 computeScroll 中調用 continueSettling 方法,continueSettling 會每次移動一定的偏移量,直到返回false。

還有一些常用的方法,這里就不具體介紹了,將要用到的時候再說。

參考鏈接

1. 鴻洋:Android ViewDragHelper完全解析 自定義ViewGroup神器

2. ViewDragHelper(一)— 介紹及使用(入門篇)

3. ViewDragHelper詳解

4. ViewDragHelper 的基本使用(一)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM