Scroller的使用及解析(滑动删除)

在学习这个之前,你首先要了解android的消息机制Android的坐标系统android View绘制流程

  1. scrollBy 个 scrollTo的区别
    scrollTo:相对View的初始位置移动的距离。
    scrollBy:相对当前位置移动的距离。
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
	//mScrollX 当前的X偏移量,mScrollY 当前的Y偏移量
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }
  1. 在实际过程中,例如我们想使控件水平向右移动200dp,那么使用方式:
scrollTo(-200,0;

此时用户可能会很奇怪为什么是-200,因为这个偏移量是相对屏幕左上角移出屏幕外的距离。移出屏幕外的为正,屏幕内的为负。

成员变量mScrollX, mScrollY,相对屏幕左上角已经移出屏幕之外的距离。

Note:假如你给一个LinearLayout调用scrollTo()方法,并不是LinearLayout滚动,而是LinearLayout里面的内容进行滚动,比如你想对一个按钮进行滚动,直接用Button调用scrollTo()一定达不到你的需求,大家可以试一试,如果真要对某个按钮进行scrollTo()滚动的话,我们可以在Button外面包裹一层Layout,然后对Layout调用scrollTo()方法。

  1. startScroll && computeScrollOffset 方法
public class ScrollerLayout extends ViewGroup {
...
  @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}

View滚动的实现原理,我们先调用Scroller的startScroll()方法来进行一些滚动的初始化设置,然后迫使View进行绘制,我们调用View的invalidate()或postInvalidate()就可以重新绘制View,绘制View的时候会触发computeScroll()方法,我们重写computeScroll(),在computeScroll()里面先调用Scroller的computeScrollOffset()方法来判断滚动有没有结束,如果滚动没有结束我们就调用scrollTo()方法来进行滚动,该scrollTo()方法虽然会重新绘制View,但是我们还是要手动调用下invalidate()或者postInvalidate()来触发界面重绘,重新绘制View又触发computeScroll(),所以就进入一个循环阶段,这样子就实现了在某个时间段里面滚动某段距离的一个平滑的滚动效果也许有人会问,为什么不直接调用scrollTo()方法来实现滚动,其实直接调用是可以,只不过scrollTo()是瞬间滚动的,给人的用户体验不太好,所以Android提供了Scroller类实现平滑滚动的效果。为了方面大家理解,我画了一个简单的scroll实现滚动的原理图
在这里插入图片描述

  1. Scroller 使用

Scroller使用基本步骤:

  1. 创建Scroller的实例
  2. (可选)判断刷新时机并调用startScroll()方法来初始化滚动数据并刷新界面
  3. (可选)重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
package com.example.qwe;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

public class ScrollerLayout extends ViewGroup {
    //滚动实例
    private Scroller mScroller;
    //判定为移动的最小像素
    private int mTouchSlop;
    //手机按下的时候屏幕坐标
    private float mXDown;
    //按住移动的时候屏幕坐标
    private float mXMove;
    //上次触发ACTION_MOVE事件时的屏幕坐标
    private float mXLastMove;
    //界面可滚动的左边界
    private int mLeftBorder;
    //界面可滚动的右边界
    private int mRightBorder;

    public ScrollerLayout(Context ctx, AttributeSet attrs){
        super(ctx,attrs);
        // 第一步,创建Scroller的实例
        mScroller = new Scroller(ctx);
        ViewConfiguration viewConfiguration = ViewConfiguration.get(ctx);
        // 获取TouchSlop值
        mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
    }


    //view绘制三部曲:OnMeasure,OnLayout,OnDraw
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int nChildCount = getChildCount();
        for (int i = 0;i < nChildCount;++i){
            View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                measureChild(childView,widthMeasureSpec,heightMeasureSpec);
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(changed){
            int nChildCount = getChildCount();
            for (int i = 0;i < nChildCount;++i){
                View childView = getChildAt(i);
                //水平布局这三个控件
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }

            if(nChildCount > 0){
                mLeftBorder = getChildAt(0).getLeft();
                mRightBorder = getChildAt(getChildCount() - 1).getRight();
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mXLastMove = ev.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mXDown);
                //如果达到最小移动单位,则拦截ACTION_MOVE事件
                if(diff > mTouchSlop)
                    return true;
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();
                int scrolledX = (int)(mXLastMove - mXMove);
                if(getScrollX() + scrolledX < mLeftBorder){
                    scrollTo(mLeftBorder,0);
                    return true;
                }
                else if(getScrollX() + getWidth() + scrolledX > mRightBorder){
                    scrollTo(mRightBorder - getWidth(),0);
                    return true;
                }
                scrollBy(scrolledX,0);
                mXLastMove = mXMove;
                break;

            case MotionEvent.ACTION_UP:
                // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                //这里为什么没用使用scrollTo是为了实现缓冲效果,而不是一下子跳跃滚动
                // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
                //为了配合调用startScroll需要重写computeScroll方法
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
        // 返回false表示滚动完成
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
    
    //跳转到指定页
	public void goToPage(int targetIndex){
        int dx = targetIndex * getWidth() - getScrollX();
        mScroller.startScroll(getScrollX(), 0, dx, 0);
        invalidate();
    }
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ScrollerActivity">

    <com.example.qwe.ScrollerLayout
        android:id="@+id/scroll_Layout"
        android:layout_width="match_parent"
        android:layout_height="100dp">
        <Button
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="This is first child view"/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="This is second child view"/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="This is third child view"/>
    </com.example.qwe.ScrollerLayout>


        <Button
            android:id="@+id/btn_go_0"
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:text="Go To first child view"/>

        <Button
            android:id="@+id/btn_go_1"
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:text="Go To second child view"/>

        <Button
            android:id="@+id/btn_go_2"
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:text="Go To third child view"/>

</LinearLayout>

在这里插入图片描述
可能遇见问题:
实际操作过程中我们可能会遇见ACTION_MOVE事件无法触发的现象,这是因为子View没有处理ACTION_DOWN的原因:https://blog.csdn.net/dreamsever/article/details/53907691
参考博客:
Scroller原理: https://www.jianshu.com/p/543b88fa609c
Scroller使用:https://blog.csdn.net/guolin_blog/article/details/48719871


下面是一个防QQ滑动删除控件的实现,如果在RecycleView中使用的话,需要调用setDragListener,处理Drag实现:
drag_item.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="60dp">

    <com.test.demo.DragControl
        android:id="@+id/drag_item"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView android:layout_height="match_parent"
            android:id="@+id/item_text"
            android:singleLine="true"
            android:layout_width="match_parent"
            android:clickable="true"
            android:background="@color/colorPrimary"/>

        <Button android:layout_height="match_parent"
            android:id="@+id/item_button"
            android:text="Del"
            android:layout_width="200dp"
            android:background="#ff00ff"/>

    </com.test.demo.DragControl>

</LinearLayout>

DragControl.java

package com.test.demo;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.Scroller;

import androidx.annotation.Nullable;

public class DragControl extends ViewGroup {
    public interface IDragEvent{
        void onDragEnd(DragControl dragControl,boolean bDrag);
        void onDragBegin(DragControl dragControl);
        void onDragCancel(DragControl dragControl);
    }

    static public class DefaultDragEvent implements IDragEvent{
        private DragControl dragControl = null;
        @Override
        public void onDragEnd(DragControl dragControl, boolean bDrag) {
            if(bDrag){
                this.dragControl = dragControl;
            }
            else{
                this.dragControl = null;
            }
        }

        @Override
        public void onDragBegin(DragControl dragControl) {
            if(this.dragControl != null) {
                this.dragControl.reset();
                this.dragControl = null;
            }
        }

        @Override
        public void onDragCancel(DragControl dragControl) {
            if(this.dragControl != null) {
                this.dragControl.reset();
                this.dragControl = null;
            }
        }
    }

    private IDragEvent iDragEvent = null;
    //滚动条
    private Scroller mScroller;
    //按住移动的时候屏幕坐标
    private float mXMove;
    //按下的时候屏幕坐标
    private float mXDown = 0;
    //最后移动的Move坐标
    private float mXLastMove = 0;
    //判定为移动的最小像素
    private int mTouchSlop = 0;
    //UI边界
    private int mLeftBorder = 0;
    private int mRightBorder = 0;
    //是否开始drag
    private boolean mbDrag = false;

    public DragControl(Context context, AttributeSet attrs) {
        super(context, attrs);

        mScroller = new Scroller(context);
        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int nChildeCount = getChildCount();
        for (int i = 0;i < nChildeCount;++i){
            View child = getChildAt(i);
            if(child.getVisibility() != View.GONE){
                measureChild(child,widthMeasureSpec,heightMeasureSpec);
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        
            int nChildCount = getChildCount();
            int nOffsetLeft = 0;
            for (int i = 0;i < nChildCount;++i){
                View child = getChildAt(i);
                child.layout(nOffsetLeft, 0, nOffsetLeft + child.getMeasuredWidth(), child.getMeasuredHeight());
                nOffsetLeft += child.getMeasuredWidth();
            }

            if(nChildCount > 0){
                mLeftBorder = getChildAt(0).getLeft();
                mRightBorder = nOffsetLeft;
            }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("kcc","onInterceptTouchEvent action:" + String.valueOf(ev.getAction()));
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                mbDrag = false;
                mXLastMove = mXDown = ev.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mXDown);
                if(diff >mTouchSlop){
                    return true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if(iDragEvent != null)
                    iDragEvent.onDragCancel(this);
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("kcc","onTouchEvent action:" + String.valueOf(event.getAction()));
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                getParent().requestDisallowInterceptTouchEvent(true);
                if(!mbDrag){
                    mbDrag = true;
                    if(iDragEvent != null)
                        iDragEvent.onDragBegin(this);
                }
                mXMove = event.getRawX();
                int xOffset = (int)(mXLastMove - mXMove);
                //左右边界判断
                if(getScrollX() + xOffset < mLeftBorder){
                    scrollTo(mLeftBorder,0);
                    return true;
                }
                else if(getScrollX() + getWidth() + xOffset > mRightBorder){
                    scrollTo(mRightBorder - getWidth(),0);
                    return true;
                }
                scrollBy(xOffset,0);
                mXLastMove = mXMove;

                break;
            case MotionEvent.ACTION_UP:
                ViewParent parent = getParent();
                //计算当前在哪个控件位置
                int nOffsetX = Math.abs(getScrollX()) + getWidth();
                int nOffsetRight = 0,targetIndex  = 0,lastchildWidth = 0;
                int nChildCount = getChildCount();
                for (int i = 0;i < nChildCount;++i){
                    View child = getChildAt(i);
                    lastchildWidth = child.getMeasuredWidth();

                    //如果当前控件滑出距离大于1/2则显示当前滑出控件,否则显示前面一个控件
                    if(nOffsetX <= nOffsetRight + lastchildWidth){
                        if(nOffsetX <= nOffsetRight + lastchildWidth/2){
                            targetIndex = Math.max(0,i -1);
                        }
                        else{
                            targetIndex = i;
                            nOffsetRight += lastchildWidth;
                        }
                        break;
                    }
                    nOffsetRight += lastchildWidth;
                }

                int dx = nOffsetRight - getWidth() - getScrollX();
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                if(iDragEvent != null)
                    iDragEvent.onDragEnd(this,targetIndex != 0);

                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    public void reset(){
        scrollTo(mLeftBorder,0);
    }

    public void setDragListener(IDragEvent iDragEvent){
        this.iDragEvent = iDragEvent;
    }
}

调用:

	//dragEvent属于Adapter成员变量
 DragControl.IDragEvent dragEvent = new DragControl.DefaultDragEvent()
 drag_item = itemView.findViewById(R.id.drag_item);
 drag_item.setDragListener(IDragEvent);

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值