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

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1. 首先,在布局文件中定义BottomSheet和RecycleView: ``` <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.bottomsheet.BottomSheetBehavior android:id="@+id/bottom_sheet" android:layout_width="match_parent" android:layout_height="wrap_content" app:behavior_hideable="true" app:behavior_peekHeight="0dp" app:layout_behavior="@string/bottom_sheet_behavior"> <LinearLayout android:id="@+id/bottom_sheet_content" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:orientation="vertical"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> </com.google.android.material.bottomsheet.BottomSheetBehavior> </androidx.coordinatorlayout.widget.CoordinatorLayout> ``` 2. 在Activity中,初始化BottomSheetBehavior和RecycleView: ``` private BottomSheetBehavior bottomSheetBehavior; private RecyclerView recyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new MyAdapter()); bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.bottom_sheet)); bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); // 设置地图的触摸事件监听器,用于处理拖拽事件 AMap aMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMap(); aMap.setOnMapTouchListener(new AMap.OnMapTouchListener() { @Override public void onTouch(MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) { if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } } } }); } ``` 3. 在MyAdapter中,实现RecyclerView的内容: ``` public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> { private List<String> data = new ArrayList<>(); public MyAdapter() { for (int i = 0; i < 20; i++) { data.add("Item " + i); } } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false); return new MyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { holder.textView.setText(data.get(position)); } @Override public int getItemCount() { return data.size(); } } public class MyViewHolder extends RecyclerView.ViewHolder { public TextView textView; public MyViewHolder(@NonNull View itemView) { super(itemView); textView = itemView.findViewById(R.id.text_view); } } ``` 4. 在布局文件中定义item_layout.xml: ``` <?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="?attr/listPreferredItemHeight" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/text_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Item" android:textSize="16sp" android:textStyle="bold" /> </LinearLayout> ``` 5. 运行程序,拖动地图时,BottomSheet会自动展开,显示RecyclerView的内容。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值