Android使用ViewDragHelper實現簡單的view拖拽和吸邊功能

工作了幾年,最開始做的是安卓開發,後面的做了一段時間逆向和sdk開發,一直沒有系統的整理自己的知識,打算從本篇博客開始,陸續複習並記錄一下自己的安卓知識

一直不知道怎麼排版,先湊合着弄下

實現效果,gif上傳被壓扁了

實現效果

 

ViewDragHelper的用法

viewDragHelper是一個安卓自帶的處理拖拽的工具

先看一下viewDragHelper的創建步驟

 public static ViewDragHelper create(@NonNull ViewGroup forParent, @NonNull ViewDragHelper.Callback cb)

ViewGroup傳的就是需要操作的View容器,一般我們把代碼寫在自定義View內,這裏也就直接傳this

ViewDragHelper.Callback 這個是處理拖動邏輯的核心模塊,具體的方法有

public boolean tryCaptureView(@NonNull View view, int i)

這是判定規則,只有return true的時候纔會去執行後續的拖動操作

這裏的view是容器內被touch到的字view,只有這個view和我們需要拖動的view爲同一個的時候我們才認爲是匹配的

也就是 return  myView == view;

public int getViewHorizontalDragRange(@NonNull View child);

public int getViewVerticalDragRange(@NonNull View child);

容器內可以拖動的區間,只有大於0的時候纔可以執行相應方向的操作,一般沒有特殊要求,我們會把這個返回值設爲當期容器的寬和高

 public int clampViewPositionHorizontal(@NonNull View child, int left, int dx);

 public int clampViewPositionVertical(@NonNull View child, int top, int dy);

這個返回的是被操作的view在橫向或縱向所能滑出的最大距離,或者說,在x或y方向的最左或最右,最上或最下所能達到的位置,這個可以有正負,比如x方向負就直接超出左邊屏幕了哈

 public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel);

這個是view被釋放時候執行的操作,我們可以操作view回到指定的位置或者保持不變等

在介紹ViewDragHelper的部分方法

public boolean settleCapturedViewAt(int finalLeft, int finalTop)

把我們所操作的view平滑的滑動到指定的位置

public boolean smoothSlideViewTo(@NonNull View child, int finalLeft, int finalTop)

和上面的settleCapturedViewAt效果類似,這個可以傳被滑動的指定view

但是單純用這兩個方法會發現view並沒有變動,這就需要搭配下面的方法一起使用

public boolean continueSettling(boolean deferCallbacks);

public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev);

public void processTouchEvent(@NonNull MotionEvent ev);

拖動處理三件套,需要讓viewDragHelper處理手勢和scroller圓滑過渡

實現

首先我們定義一些擴展的變量

private View dragView;                //被拖拽的view
private ViewDragHelper viewDragHelper;
private int mWidth;                 //容器的寬度
private int mHeight;                //容器的高度
private int mChildWidth;            //拖拽的View寬度
private int mChildHeight;           //拖拽的View高度
private boolean onDrag = true;      //是否正在被拖拽
private boolean dragEnable = true;  //是否是可以拖拽的
private boolean sideEnable = true;  //是否吸邊

private final int NONE = -1;

private int topFinalOffset;        //拖拽超出上邊界後,釋放後回到的top位置
private int bottomFinalOffset;     //拖拽超出下邊界後,釋放後回到的bottom位置
private int leftFinalOffset;       //拖拽超出左邊界後,釋放後回到的left位置
private int rightFinalOffset;      //拖拽超出右邊界後,釋放後回到的right位置
    
private int leftDragOffset = NONE;            //能向左拖拽的最大距離
private int rightDragOffset = NONE;            //能向右拖拽的最大距離
private int topDragOffset = NONE;             //能向上拖拽的最大距離
private int bottomDragOffset = NONE;             //能向下拖拽的最大距離

先初始化並獲取一些參數

private void init() {
        viewDragHelper = ViewDragHelper.create(this, new MyDragCallBack());
}

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        //獲取裝載容器的寬高以及拖拽view的寬高
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        mChildHeight = dragView.getMeasuredHeight();
        mChildWidth = dragView.getMeasuredWidth();

        //默認最多可以拖拽1/2的view出屏幕
        leftDragOffset = leftDragOffset == NONE ? mChildWidth / 2 : leftDragOffset;
        rightDragOffset = rightDragOffset == NONE ? mChildWidth / 2 : rightDragOffset;
        topDragOffset = topDragOffset == NONE ? mChildHeight / 2 : topDragOffset;
        bottomDragOffset = bottomDragOffset == NONE ? mChildHeight / 2 : bottomDragOffset;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (lastChildX == 0 && lastChildY == 0) {
            calLayoutOffset();
        }
        //把view佈局到相應的位置,當然第一次就是在左上角,後續位置會發生變化
        dragView.layout(lastChildX, lastChildY, lastChildX + mChildWidth, lastChildY + mChildHeight);

    }

    public void calLayoutOffset() {
        //把x,y初始化設置爲最終要停留在左上角的位置
        lastChildX =leftFinalOffset;
        lastChildY =topFinalOffset;
    }

@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() != 1) {
            throw new RuntimeException("child size must be 1");
        }
        dragView = getChildAt(0);
        dragView.bringToFront();
    }
 private Rect mRect = new Rect();

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (dragEnable) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    int x = (int) ev.getX();
                    int y = (int) ev.getY();
                    dragView.getHitRect(mRect);
                    onDrag = mRect.contains(x, y);
                    //如果按下的點在dragView內,則認爲是拖動有效,執行viewDragHelper的方法
                    break;
            }

            if (onDrag) return viewDragHelper.shouldInterceptTouchEvent(ev);
        }
        return super.onInterceptTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (dragEnable) {
            if (onDrag) {
                viewDragHelper.processTouchEvent(event);
                return true;
            }
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        if (dragEnable) {
            if (viewDragHelper.continueSettling(true)) {
                invalidate();
            }
        }
    }

    public static int dp2px(Context context, float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                d

準備做完了,那麼就要進行操作的代碼了

 private class MyDragCallBack extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(@NonNull View view, int i) {
            return dragView == view;
        }

        //以橫向拖動爲例
        //left是當前拖動view的左邊的座標
        //我們要做的就是讓 left >= 最左的距離  同時  left <= 最右的距離
        //就是我們設置的leftDragOffset 和 rightDragOffset 
        @Override
        public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
            leftDragOffset = leftDragOffset > mChildWidth ? mChildWidth : leftDragOffset;
            rightDragOffset = rightDragOffset > mChildWidth ? mChildWidth : rightDragOffset;

            return clamp(left, -leftDragOffset, mWidth - mChildWidth + rightDragOffset);

        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            topDragOffset = topDragOffset > mChildHeight ? mChildHeight : topDragOffset;
            bottomDragOffset = bottomDragOffset > mChildHeight ? mChildHeight : bottomDragOffset;

            return clamp(top, -topDragOffset, mHeight - mChildHeight + bottomDragOffset);

        }

        @Override
        public int getViewVerticalDragRange(@NonNull View child) {
//            return super.getViewVerticalDragRange(child);
            return mHeight;
        }

        @Override
        public int getViewHorizontalDragRange(@NonNull View child) {
//            return super.getViewHorizontalDragRange(child);
            return mWidth;
        }

        @Override
        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            if (sideEnable) {
                super.onViewReleased(releasedChild, xvel, yvel);

            //如果top小於topFinOffset則取topFinalOffset
            //如果bottom大於最大的offset則取限制的最大bottom
                int finalTop = dragView.getTop() <= topFinalOffset ? topFinalOffset : dragView.getBottom() >= mHeight - bottomFinalOffset ? mHeight - dragView.getMeasuredHeight() - bottomFinalOffset : dragView.getTop();
                lastChildY = finalTop;
                //根據left和view的一半進行界定,選擇是最終停留在左邊還是右邊
                if (Math.abs(dragView.getLeft()) <= (getMeasuredWidth() - dragView.getMeasuredWidth()) / 2) {
                    lastChildX = leftFinalOffset;
                    //平滑過渡到相應位置
                    viewDragHelper.settleCapturedViewAt(lastChildX, finalTop);
                } else {
                    lastChildX = getMeasuredWidth() - dragView.getMeasuredWidth() - rightFinalOffset;
                    viewDragHelper.settleCapturedViewAt(lastChildX,
                            finalTop);
                }
                invalidate();
            } else {
                lastChildX = dragView.getLeft();
                lastChildY = dragView.getTop();
            }

        //把拖拽的標記定爲false
            onDrag = false;


        }
    }


    //其實就是 當前和最大值取最小 同時和最小值取最大
    //如果value在兩者之間直接返回value
    //如果value比最小值小,則返回min
    //如果value比最大值大,則返回max  所以是滿足我們的條件的
    private int clamp(int value, int min, int max) {
        return Math.max(min, Math.min(max, value));
    }

然後建立佈局文件

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

    <com.example.viewdemo.view.FloatLayout
        android:id="@+id/layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <View
            android:id="@+id/view"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#f00"
            />
    </com.example.viewdemo.view.FloatLayout>

</LinearLayout>

引用代碼

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        floatLayout = findViewById(R.id.layout);
        mView = findViewById(R.id.view);

        floatLayout.enableDrag(true);
        floatLayout.enableSide(true);

        //設置最大可拖拽的偏移量
        floatLayout.setFinalDragOffsets(80,80,80,80);
        //設置最終停留的位置偏移
        floatLayout.setFinalOffsets(-50);

        floatLayout.requestLayout();
        floatLayout.invalidate();


        mView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FloatLaytoutActivity.this,"view被點擊了",Toast.LENGTH_LONG).show();
            }
        });
    }

完整的代碼

public class FloatLayout extends FrameLayout {

    private final int NONE = -1;
    private View dragView;
    private ViewDragHelper viewDragHelper;
    private int mWidth;
    private int mHeight;
    private int mChildWidth;
    private int mChildHeight;
    private boolean onDrag = true;
    private boolean dragEnable = true;
    private boolean sideEnable = true;  //是否吸邊

    private int lastChildX;
    private int lastChildY;

    private int topFinalOffset;
    private int bottomFinalOffset;
    private int leftFinalOffset;
    private int rightFinalOffset;


    private int leftDragOffset = NONE;
    private int rightDragOffset = NONE;
    private int topDragOffset = NONE;
    private int bottomDragOffset = NONE;

    public FloatLayout(@NonNull Context context) {
        this(context, null);
    }

    public FloatLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FloatLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init() {
        viewDragHelper = ViewDragHelper.create(this, new MyDragCallBack());
    }

    public void setBottomDragOffset(int dpValue) {
        this.bottomDragOffset = dp2px(getContext(), dpValue);
    }

    public void setTopDragOffset(int dpValue) {
        this.topDragOffset = dp2px(getContext(), dpValue);
    }

    public void setLeftDragOffset(int dpValue) {
        this.leftDragOffset = dp2px(getContext(), dpValue);
    }

    public void setRightDragOffset(int dpValue) {
        this.rightDragOffset = dp2px(getContext(), dpValue);
    }

    public void setFinalOffsets(int value) {
        setFinalOffsets(value, value, value, value);
    }

    //拖拽能偏移出父容器的值,取正數
    public void setFinalDragOffsets(int left, int top, int right, int bottom) {
        setLeftDragOffset(left);
        setTopDragOffset(top);
        setRightDragOffset(right);
        setBottomDragOffset(bottom);
    }

    public void setFinalOffsets(int left, int top, int right, int bottom) {
        setLeftFinalOffset(left);
        setTopFinalOffset(top);
        setRightFinalOffset(right);
        setBottomFinalOffset(bottom);
//        calLayoutOffset();
    }

    public void setLeftFinalOffset(int dpValue) {
        this.leftFinalOffset = dp2px(getContext(), dpValue);
    }

    public void setRightFinalOffset(int dpValue) {
        this.rightFinalOffset = dp2px(getContext(), dpValue);
    }

    public void setBottomFinalOffset(int dpValue) {
        this.bottomFinalOffset = dp2px(getContext(), dpValue);
    }

    public void setTopFinalOffset(int dpValue) {
        this.topFinalOffset = dp2px(getContext(), dpValue);
    }

    public void enableDrag(boolean value) {
        dragEnable = value;
    }

    public void enableSide(boolean value) {
        sideEnable = value;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        mChildHeight = dragView.getMeasuredHeight();
        mChildWidth = dragView.getMeasuredWidth();

        leftDragOffset = leftDragOffset == NONE ? mChildWidth / 2 : leftDragOffset;
        rightDragOffset = rightDragOffset == NONE ? mChildWidth / 2 : rightDragOffset;
        topDragOffset = topDragOffset == NONE ? mChildHeight / 2 : topDragOffset;
        bottomDragOffset = bottomDragOffset == NONE ? mChildHeight / 2 : bottomDragOffset;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (lastChildX == 0 && lastChildY == 0) {
            calLayoutOffset();
        }
        dragView.layout(lastChildX, lastChildY, lastChildX + mChildWidth, lastChildY + mChildHeight);
    }

    public void calLayoutOffset() {
        lastChildX =leftFinalOffset;
        lastChildY =topFinalOffset;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() != 1) {
            throw new RuntimeException("child size must be 1");
        }
        dragView = getChildAt(0);
        dragView.bringToFront();
    }


    private class MyDragCallBack extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(@NonNull View view, int i) {
            return dragView == view;
        }

        @Override
        public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
            leftDragOffset = leftDragOffset > mChildWidth ? mChildWidth : leftDragOffset;
            rightDragOffset = rightDragOffset > mChildWidth ? mChildWidth : rightDragOffset;

            return clamp(left, -leftDragOffset, mWidth - mChildWidth + rightDragOffset);

        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            topDragOffset = topDragOffset > mChildHeight ? mChildHeight : topDragOffset;
            bottomDragOffset = bottomDragOffset > mChildHeight ? mChildHeight : bottomDragOffset;

            return clamp(top, -topDragOffset, mHeight - mChildHeight + bottomDragOffset);

        }

        @Override
        public int getViewVerticalDragRange(@NonNull View child) {
//            return super.getViewVerticalDragRange(child);
            return mHeight;
        }

        @Override
        public int getViewHorizontalDragRange(@NonNull View child) {
//            return super.getViewHorizontalDragRange(child);
            return mWidth;
        }

        @Override
        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            if (sideEnable) {
                super.onViewReleased(releasedChild, xvel, yvel);

                int finalTop = dragView.getTop() <= topFinalOffset ? topFinalOffset : dragView.getBottom() >= mHeight - bottomFinalOffset ? mHeight - dragView.getMeasuredHeight() - bottomFinalOffset : dragView.getTop();
                lastChildY = finalTop;
                if (Math.abs(dragView.getLeft()) <= (getMeasuredWidth() - dragView.getMeasuredWidth()) / 2) {
                    lastChildX = leftFinalOffset;
                    viewDragHelper.settleCapturedViewAt(lastChildX, finalTop);
                } else {
                    lastChildX = getMeasuredWidth() - dragView.getMeasuredWidth() - rightFinalOffset;
                    viewDragHelper.settleCapturedViewAt(lastChildX,
                            finalTop);
                }
                invalidate();
            } else {
                lastChildX = dragView.getLeft();
                lastChildY = dragView.getTop();
            }
            onDrag = false;


        }
    }

    private int clamp(int value, int min, int max) {
        return Math.max(min, Math.min(max, value));
    }


    private Rect mRect = new Rect();

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (dragEnable) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    int x = (int) ev.getX();
                    int y = (int) ev.getY();
                    dragView.getHitRect(mRect);
                    onDrag = mRect.contains(x, y);
                    break;
            }

            if (onDrag) return viewDragHelper.shouldInterceptTouchEvent(ev);
        }
        return super.onInterceptTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (dragEnable) {
            if (onDrag) {
                viewDragHelper.processTouchEvent(event);
                return true;
            }
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        if (dragEnable) {
            if (viewDragHelper.continueSettling(true)) {
                invalidate();
            }
        }
    }

    public static int dp2px(Context context, float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal, context.getResources().getDisplayMetrics());
    }

}

附上demo地址 https://github.com/gouptosee/ViewDragHelperDemo

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章