View的相关分发事件

本文深入探讨了Android中视图的交互机制,包括Configuration对象、ViewConfiguration、Fling手势、GestureDetector、VelocityTracker、Scroller及ViewDragHelper的使用方法与原理。通过实例展示了如何利用这些工具实现自定义视图的拖拽、滑动、手势识别等功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


千言无语抵不过一张图,这图详细说明相关分发事件处理:


说到了事件处理,必定少不了相关坐标系

在自定义View的时候,一般都会使用到一些Android系统提供的工具,这些工具封装了一些常用的方法:如拖拽View、计算滑动速度、View滚动、手势处理...。常用系统的封装好的工具

Configuration

Configuration对象获取:Configuration config = getResources().getConfiguration();

Configuration用来描述设备的配置信息

比如用户的配置信息:local和scaling等等

比如设备的相关信息:输入模式、屏幕大小、屏幕方向等等

采用如下方式可以获取相关需要信息:

字段

public static finalCreator<Configuration> CREATOR
public int densityDpi

渲染的目标屏幕密度对应于密度资源限定符

public float fontScale

当前用户对字体缩放因子的偏好,相对于基本密度缩放

public int hardKeyboardHidden

指示硬键盘是否已隐藏的标志。

public int keyboard

键盘连接到设备的类型。

public int keyboardHidden

指示是否有任何键盘可用的标志。

public Locale locale

该字段在API级别24.不要设置或直接读取这被废弃了。使用getLocales()和 setLocales(LocaleList)如果只需要在主区域, getLocales().get(0)是目前首选的访问。

public int mcc

IMSI MCC(移动国家代码),对应于mcc资源限定符。

public int mnc

IMSI MNC(移动网络代码),对应于mnc资源限定符

public int navigation

在设备上可用的导航方法的类型。

public int navigationHidden

指示是否有任何5-way或DPAD导航可用的标志

public int orientation

屏幕的总体方向。

public int screenHeightDp

可用屏幕空间的当前高度(以dp为单位),对应于屏幕高度资源限定符。

public int screenLayout

屏幕的整体布局的位掩码。

public int screenWidthDp

可用屏幕空间的当前宽度(以dp为单位),对应于屏幕宽度资源限定符。

public int smallestScreenWidthDp

应用程序在正常操作中将看到的最小屏幕大小,对应于最小屏幕宽度资源限定符。

public int touchscreen

触摸屏的类型连接到设备。

public int uiMode

ui模式的位掩码。

StringBuilder stringBuilder = new StringBuilder();
        //获取Configuration对象
        Configuration configuration = getResources().getConfiguration();
        //获取国家码
        int countryCode = configuration.mcc;
        stringBuilder.append("国家码:").append(countryCode + "\n");
        //获取网络码
        int networkCode = configuration.mnc;
        stringBuilder.append("网络码:").append(networkCode + "\n");
        //判断横竖屏
        if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT){ //竖屏
            stringBuilder.append("屏幕竖屏\n");
        }else if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){ //横屏
            stringBuilder.append("屏幕横屏\n");
        }
        DisplayMetrics metrics = this.getApplicationContext().getResources().getDisplayMetrics();
        float density = metrics.density;
        stringBuilder.append("屏幕密度:").append(density + "\n");
        int densityDpi = configuration.densityDpi;
        stringBuilder.append("目标屏幕密度(指的是倍数):").append(densityDpi + "\n");
        float fontScale = configuration.fontScale;
        stringBuilder.append("字体缩放情况:").append(fontScale + "\n");
需注意      目标屏幕密度(单位像素)= 屏幕密度x160;
为什么要乘以160呢?这就涉及到了手机屏幕密度问题

屏幕密度:单位英寸面积上的像素点数

屏幕物理尺寸 = Math.sqrt(Math.pow(宽,2)+Math.pow(高, 2))/DPI  =(((屏幕宽度的分辨率的平方 + 屏幕高度的分辨率的平方)之和 )在开方 )/ (除以手机屏幕的尺寸) = 手机屏幕对角尺寸

ldpi:   屏幕密度为120的手机设备
mdpi: 屏幕密度为160的手机设备(此为baseline,其他均以此为基准,在此设备上,1dp = 1px)
hdpi:  屏幕密度为240的手机设备
xhdpi: 屏幕密度为320的手机设备
xxhdpi:屏幕密度为480的手机设备

这篇描述了Android中屏幕密度和图片大小的关系分析想了解可以看一下


ViewConfiguration

看完Configuration再来瞅ViewConfiguration。这两者的名字有些像,差了一个View;咋一看,还以为它俩是继承关系,其实不然

ViewConfiguration提供了一些自定义控件用到的标准常量,比如尺寸大小,滑动距离,敏感度等等。

ViewConfiguration对象获取:ViewConfiguration viewConfiguration = ViewConfiguration.get(this);

ViewConfiguration源码分析

公共方法

staticViewConfiguration get(Context context)
static long getDefaultActionModeHideDuration()
static int getDoubleTapTimeout()
双击间隔时间.在该时间内是双击,否则是单击
static int getEdgeSlop()

This method was deprecated in API level 3.使用getScaledEdgeSlop()来代替.

static int getFadingEdgeLength()

This method was deprecated in API level 3.使用getScaledFadingEdgeLength()来代替。

static long getGlobalActionKeyTimeout()

This method was deprecated in API level 20. This timeout should not be used by applications

static int getJumpTapTimeout()
static int getKeyRepeatDelay()
static int getKeyRepeatTimeout()
重复按键的时间
static int getLongPressTimeout()
按住状态转变为长按状态需要的时间
static int getMaximumDrawingCacheSize()

This method was deprecated in API level 3.使用getScaledMaximumDrawingCacheSize()来代替。

static int getMaximumFlingVelocity()

This method was deprecated in API level 4.使用getScaledMaximumFlingVelocity()来代替。

static int getMinimumFlingVelocity()

This method was deprecated in API level 3.使用getScaledMinimumFlingVelocity()来代替。

static int getPressedStateDuration()
int getScaledDoubleTapSlop()
int getScaledEdgeSlop()
int getScaledFadingEdgeLength()
int getScaledMaximumDrawingCacheSize()

以字节表示的最大冲压高速缓存大小。

int getScaledMaximumFlingVelocity()
int getScaledMinimumFlingVelocity()
int getScaledOverflingDistance()
int getScaledOverscrollDistance()
int getScaledPagingTouchSlop()
int getScaledScrollBarSize()
int getScaledTouchSlop()
int getScaledWindowTouchSlop()
static int getScrollBarFadeDuration()
滚动条褪去的持续时间
static int getScrollBarSize()

This method was deprecated in API level 3.使用getScaledScrollBarSize()来代替。

static int getScrollDefaultDelay()
滚动条褪去的延迟时间
static float getScrollFriction()

施加到卷轴和甩摩擦的量。

static int getTapTimeout()
static int getTouchSlop()

This method was deprecated in API level 3.使用getScaledTouchSlop()来代替。

static int getWindowTouchSlop()

This method was deprecated in API level 3.使用getScaledWindowTouchSlop()来代替。

static long getZoomControlsTimeout()

应在屏幕上显示缩放控件的时间量,以毫秒为单位。

boolean hasPermanentMenuKey()

报告设备是否有永久的菜单键可供用户使用。

StringBuilder strBuilder = new StringBuilder();
        ViewConfiguration viewConfiguration = ViewConfiguration.get(this);
        //获取touchSlope.该值表示系统所能识别出的被认为是滑动的最小距离
        int touchSlop = viewConfiguration.getScaledTouchSlop();
        strBuilder.append("系统识别最小滑动距离:").append(touchSlop+"\n");
        //获取Fling速度的最小值和最大值
        int minimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
        strBuilder.append("Fling速度最小值:").append(minimumVelocity + "\n");
        int maximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        strBuilder.append("Fling速度最大值:").append(maximumVelocity + "\n");
        //判断是否有物理按键
        boolean isHavePermanentMenuKey = viewConfiguration.hasPermanentMenuKey();
        strBuilder.append("判断是否有物理按键:").append(isHavePermanentMenuKey + "\n");
        mTv.setText(strBuilder.toString());

Fling

     当用户手指快速划过屏幕,然后快速离开屏幕时,系统会判定用户执行了一个Fling手势。视图会快速滚动,并且在手指离开屏幕之后也会滚动一段时间。Drag表示手指滑动了多少距离,界面跟着显示多少距离,而Fling是根据滑动方向与轻重还会自动滑动一段距离。Fling手势在android交互设计中应该非常广泛:电子书的滑动翻页、ListView滑动删除item、滑动解锁等,Fling就先说这么多先,后面会继续说到。


GestureDetector

官方解释

使用提供的MotionEvent检测各种手势和事件。 GestureDetector.OnGestureListener回调将在特定运动事件发生时通知用户。 此类只应与通过触摸报告的MotionEvent(不用于轨迹球事件)一起使用。
要使用此类:
      为您的视图创建GestureDetector的实例
      在onTouchEvent(MotionEvent)方法中,确保调用onTouchEvent(MotionEvent)。 在回调中定义的方法将在事件发生时执行。
如果监听onContextClick(MotionEvent),则必须在onGenericMotionEvent(MotionEvent)中调用onGenericMotionEvent(MotionEvent)。

嵌套类
interface GestureDetector.OnContextClickListener

用于在发生上下文单击时通知的侦听器。

interface GestureDetector.OnDoubleTapListener

用于在发生双击或确认单击时通知的侦听器。

interface GestureDetector.OnGestureListener

用于在发生手势时通知的侦听器。

class GestureDetector.SimpleOnGestureListener

当你只想监听所有手势的子集时,扩展的便利类。

这里就以GestureDetector手势监听类为例:

第一步:实现OnGestureListener

private class GestureListenerImpl implements GestureDetector.OnGestureListener{

        //触摸屏幕时均会调用该方法
        @Override
        public boolean onDown(MotionEvent e) {
            return false;
        }

        //手指在屏幕上按下,且未移动和松开时调用该方法
        @Override
        public void onShowPress(MotionEvent e) {

        }

        //轻击屏幕时调用该方法
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }

        //手指在屏幕上滚动时会调用该方法
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return false;
        }

        //手指长按屏幕时均会调用该方法
        @Override
        public void onLongPress(MotionEvent e) {

        }

        //手指在屏幕上拖动时会调用该方法
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return false;
        }
    }

第二步:生成GestureDetector对象

GestureDetector gestureDetector = new GestureDetector(context,new GestureListenerImpl());

第三步:将Touch事件交给GestureDetector处理

@Override
    public boolean onTouchEvent(MotionEvent event) {
        return  gestureDetector.onTouchEvent(event);
    }

VelocityTracker(速度追踪器)

官方解释

    用于跟踪触摸事件的速度,用于实现拖拽和其他这样的手势。当您要开始跟踪时,使用gets()来检索类的新实例。 使用addMovement(MotionEvent)将接收到的运动事件放入其中。当你想要确定速度调用computeCurrentVelocity(int),然后调用getXVelocity(int)和getYVelocity(int)检索每个指针id的速度。

公共方法
void addMovement(MotionEvent event)

将用户的移动添加到跟踪器。

void clear()

将速度跟踪器重置回其初始状态。

void computeCurrentVelocity(int units)

相当于调用computeCurrentVelocity(int,float),最大速度为Float.MAX_VALUE。

void computeCurrentVelocity(int units, float maxVelocity)  基于已收集的点计算当前速度。

int unitis表示速率的基本时间单位。unitis值为1的表示是,一毫秒时间单位内运动了多少个像素, unitis值为1000表示一秒(1000毫秒)时间单位内运动了多少个像素
floatVelocity表示速率的最大值

float getXVelocity(int id)

检索最后计算的X速度。

float getXVelocity()

检索最后计算的X速度。

float getYVelocity()

检索最后计算的Y速度。

float getYVelocity(int id)

检索最后计算的Y速度。

staticVelocityTracker obtain()

检索一个新的VelocityTracker对象以观察运动的速度。

void recycle()

返回一个VelocityTracker对象,以供其他人重复使用。

VelocityTracker使用步骤

第一点是在获取值前, 需要调用计算方法;

第二点是计算的速度是滑动的位移, 可能是正或负;

第三点是在计算完成后, 需要回收速度追踪器.

public class VelocityTrackerTest extends Activity {
    private TextView mInfo;

    private VelocityTracker mVelocityTracker;
    private int mMaxVelocity;

    private int mPointerId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mInfo = new TextView(this);
        mInfo.setLines(4);
        mInfo.setLayoutParams(new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, ActionBar.LayoutParams.MATCH_PARENT));
        mInfo.setTextColor(Color.WHITE);

        setContentView(mInfo);
        //获取Fling最大值
        mMaxVelocity = ViewConfiguration.get(this).getScaledMaximumFlingVelocity();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        acquireVelocityTracker(event);  //第一步
        final VelocityTracker verTracker = mVelocityTracker;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //求第一个触点的id, 此时可能有多个触点,但至少一个
                mPointerId = event.getPointerId(0);
                break;

            case MotionEvent.ACTION_MOVE:
                //获取追踪到的速度   【第二步】
                verTracker.computeCurrentVelocity(1000, mMaxVelocity);
                final float velocityX = verTracker.getXVelocity(mPointerId);
                final float velocityY = verTracker.getYVelocity(mPointerId);
                recodeInfo(velocityX, velocityY);
                break;

            case MotionEvent.ACTION_UP:
                releaseVelocityTracker();  //第三步
                break;

            case MotionEvent.ACTION_CANCEL:
                releaseVelocityTracker();  //第三步
                break;

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

    /**
     * @param event 向VelocityTracker添加MotionEvent
     * 开始速度追踪
     */
    private void acquireVelocityTracker(final MotionEvent event) {
        if(null == mVelocityTracker) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }

    /**
     * 解除速度追踪
     */
    private void releaseVelocityTracker() {
        if(null != mVelocityTracker) {
            mVelocityTracker.clear();
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    private static final String sFormatStr = "velocityX=%f\nvelocityY=%f";

    /**
     * 记录当前速度
     *
     * @param velocityX x轴速度
     * @param velocityY y轴速度
     */
    private void recodeInfo(final float velocityX, final float velocityY) {
        final String info = String.format(sFormatStr, velocityX, velocityY);
        mInfo.setText(info);
    }
}

Scroller

官方解释

这个类封装了滚动。您可以使用滚动器(Scroller或OverScroller)来收集生成滚动动画所需的数据,例如,响应fling手势。滚动条跟踪您的滚动偏移量,但它们不会自动应用这些位置到您的视图。 这是你的责任,获得和应用新的坐标,将使滚动动画看起来流畅的速度。

公共方法

void abortAnimation()

停止动画。

boolean computeScrollOffset()

当你想知道新的位置调用这个

void extendDuration(int extend)

扩展滚动动画。

void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)

基于fling手势开始滚动。

final void forceFinished(boolean finished)

将完成的字段强制为特定值。

float getCurrVelocity()

返回当前速度。

final int getCurrX()

返回滚动中的当前X偏移量。

final int getCurrY()

返回滚动中的当前Y偏移量。

final int getDuration()

返回滚动事件将花费的时间(以毫秒为单位)。

final int getFinalX()

返回滚动将结束X的位置。

final int getFinalY()

返回滚动将结束Y的位置。

final int getStartX()

返回滚动中的开始X偏移量。

final int getStartY()

返回滚动中的起始Y偏移量。

final boolean isFinished()

返回滚动器是否已完成滚动。

void setFinalX(int newX)

设置此滚动条的最终位置(X)。

void setFinalY(int newY)

设置此滚动条的最终位置(Y)。

final void setFriction(float friction)

施加到甩毛的摩擦量。

void startScroll(int startX, int startY, int dx, int dy, int duration)

通过提供起点,行进距离和滚动持续时间开始滚动。

void startScroll(int startX, int startY, int dx, int dy)

通过提供起点和行进距离开始滚动。

int timePassed()

返回自滚动开始所经过的时间。

scrollTo()和scrollBy()的关系
scrollBy( )的源码

public void scrollBy(int x, int y) {   
       scrollTo(mScrollX + x, mScrollY + y);   
} 
这就是说scrollBy( )调用了scrollTo( ),scrollBy里面参数指的是偏移量,最终起作用的是scrollTo( )方法。

scroll的本质

scrollTo( )和scrollBy( )移动的只是View的内容,而且View的背景是不移动的。

scrollTo( )和scrollBy( )方法的坐标说明

scroll向右滑动为负数,向左滑动为正数;向下为负数,向上为正数。

综合运用

public class SlideCutListView extends ListView {

    public interface RemoveListener {
        void removeItem(RemoveDirection direction, int position);
    }

    //移除item后的回调接口
    private RemoveListener mRemoveListener;
    private int screenWidth; //屏幕宽度
    private Scroller mScroller; //滚动器
    private static final int SNAP_VELOCITY = 600;
    private VelocityTracker velocityTracker; //速度追踪器
    private int mTouchSlop ; //获取touchSlope.该值表示系统所能识别出的被认为是滑动的最小距离
    private int downX;  //手指按下X轴值
    private int downY;  //手指按下Y轴值
    private int slidePostion;  //当前滑动的ListView对应下标值(position)
    private View itemView;  //获取焦点View
    private boolean isSlide = false; //是否响应滑动,默认为不响应
    /**
     * 用来指示item滑出屏幕的方向,向左或者向右,用一个枚举值来标记
     */
    private RemoveDirection removeDirection;

    // 滑动删除方向的枚举值
    public enum RemoveDirection {
        RIGHT, LEFT
    }


    public SlideCutListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        screenWidth = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getWidth();
        mScroller = new Scroller(context);
        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        mTouchSlop  = viewConfiguration.getScaledTouchSlop();
    }

    /**
     * 分发事件,主要做的是判断点击的是哪个item, 以及通过postDelayed来设置响应左右滑动事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                startVelocityTracker(ev); //开始速度追踪器
                if (!mScroller.isFinished()){
                    return super.dispatchTouchEvent(ev);
                }
                downX = (int) ev.getX();
                downY = (int) ev.getY();
                downXx = downX;
                slidePostion = pointToPosition(downX, downY);
                // 无效的下标position, 不做任何处理
                if (slidePostion == AdapterView.INVALID_POSITION){
                    return super.dispatchTouchEvent(ev);
                }
                itemView = getChildAt(slidePostion - getFirstVisiblePosition());
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(getScrollVelocity()) >  SNAP_VELOCITY
                        || (Math.abs(ev.getX() - downX)) > mTouchSlop &&
                        (Math.abs(ev.getY() - downY)) > mTouchSlop &&
                        (Math.abs(ev.getY() - downY)) < itemView.getHeight()){
                    isSlide = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                recycleVelocityTracker();
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    private int downXx;
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //响应了滑动 和 listView下标值是有效的
        if (isSlide && slidePostion != AdapterView.INVALID_POSITION){
            //父类事件不拦截该控件上的触摸事件
            requestDisallowInterceptTouchEvent(true);
            startVelocityTracker(ev);//开始速度追踪器
            int x = (int) ev.getX();
            final int action = ev.getAction();
            switch (action){
               case MotionEvent.ACTION_DOWN:
                   break;
               case MotionEvent.ACTION_MOVE:
                   //创建一个新的MotionEvent,从现有的复制
                   MotionEvent newEvent = MotionEvent.obtain(ev);
                   newEvent.setAction(MotionEvent.ACTION_CANCEL |
                           (ev.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT));
                   onTouchEvent(newEvent);
                   int deltaX = downXx - x;
                   downXx = x;
                   if ((Math.abs(ev.getX() - downX)) > mTouchSlop &&
                           (Math.abs(ev.getY() - downY)) > mTouchSlop &&
                           (Math.abs(ev.getY() - downY)) < itemView.getHeight()){
                       itemView.scrollBy(deltaX,0);
                   }else{
                       // 手指拖动itemView滚动, deltaX大于0向左滚动,小于0向右滚
                       itemView.scrollTo(0, 0);
                   }

                   //自个打这次事件消费掉了,不继续向下分发了
                   return true;
                case MotionEvent.ACTION_UP:
                    int velocityX = getScrollVelocity();
                    if (velocityX > SNAP_VELOCITY){
                        scrollRight();
                    }else if (velocityX < -SNAP_VELOCITY){
                        scrollLeft();
                    }else{
                        scrollByDistanceX();
                    }
                    recycleVelocityTracker();
                    // 手指离开的时候就不响应左右滚动
                    isSlide = false;
                    break;
           }

        }
        return super.onTouchEvent(ev);
    }

    @Override
    public void computeScroll() {
        // 调用startScroll的时候mScroller.computeScrollOffset()返回true
        if (mScroller.computeScrollOffset()){
            // 让ListView item根据当前的滚动偏移量进行滚动
            itemView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();

            // 滚动动画结束的时候调用回调接口
            if (mScroller.isFinished()) {
                if (mRemoveListener == null) {
                    throw new NullPointerException("RemoveListener is null, we should called setRemoveListener()");
                }

                itemView.scrollTo(0, 0);
                mRemoveListener.removeItem(removeDirection, slidePostion);
            }
        }
    }

    /**
     * 向左滑动,根据上面我们知道向左滑动为正值
     */
    private void scrollLeft() {
        removeDirection = RemoveDirection.LEFT;
        final int delta = (screenWidth - itemView.getScrollX());
        // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item
        mScroller.startScroll(itemView.getScrollX(), 0, delta, 0,
                Math.abs(delta));
        postInvalidate(); // 刷新itemView
    }

    /**
     * 根据手指滚动itemView的距离来判断是滚动到开始位置还是向左或者向右滚动
     */
    private void scrollByDistanceX() {
        // 如果向左滚动的距离大于屏幕的二分之一,就让其删除
        if (itemView.getScrollX() >= screenWidth / 2) {
            scrollLeft();
        } else if (itemView.getScrollX() <= -screenWidth / 2) {
            scrollRight();
        } else {
            // 滚回到原始位置,为了偷下懒这里是直接调用scrollTo滚动
            itemView.scrollTo(0, 0);
        }
    }

    /**
     * 往右滑动,getScrollX()返回的是左边缘的距离,就是以View左边缘为原点
     * 到开始滑动的距离,所以向右边滑动为负值
     */
    private void scrollRight() {
        removeDirection = RemoveDirection.RIGHT;
        int delta  = screenWidth + itemView.getScrollX();
        // 调用startScroll方法来设置一些滚动的参数,
        // 我们在computeScroll()方法中调用scrollTo来滚动item
        mScroller.startScroll(itemView.getScrollX(), 0, -delta, 0,Math.abs(delta));
        postInvalidate(); // 刷新itemView
    }

    private void startVelocityTracker(MotionEvent ev) {
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(ev);
    }

    //获取追踪到的X轴的速度
    private int getScrollVelocity() {
        velocityTracker.computeCurrentVelocity(1000);//1s
        int velocity = (int) velocityTracker.getXVelocity();
        return velocity;
    }

    /**
     * 移除用户速度跟踪器
     */
    private void recycleVelocityTracker() {
        if (velocityTracker != null) {
            velocityTracker.recycle();
            velocityTracker = null;
        }
    }

    //设置滑动删除的回调接口
    public void setRemoveListener(RemoveListener removeListener) {
        this.mRemoveListener = removeListener;
    }
}
activity的xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/darker_gray">

    <com.hh.person.customview.widget.SlideCutListView
        android:id="@+id/slideCutListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:listSelector="@android:color/transparent"
        android:cacheColorHint="@android:color/transparent">
    </com.hh.person.customview.widget.SlideCutListView>

</RelativeLayout>

adapter的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="wrap_content" >

    <TextView
        android:id="@+id/list_item"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="15dip" />

</LinearLayout>

Activity

public class ScrollerActivity extends Activity implements SlideCutListView.RemoveListener{
    private SlideCutListView slideCutListView ;
    private ArrayAdapter<String> adapter;
    private List<String> dataSourceList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scroller);
        slideCutListView = (SlideCutListView) findViewById(R.id.slideCutListView);
        init();
    }

    private void init() {
        slideCutListView.setRemoveListener(this);

        for(int i=0; i<20; i++){
            dataSourceList.add("滑动删除" + i);
        }

        adapter = new ArrayAdapter<>(this, R.layout.listview_item, R.id.list_item, dataSourceList);
        slideCutListView.setAdapter(adapter);

        slideCutListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                                    int position, long id) {

            }
        });
    }


    //滑动删除之后的回调方法
    @Override
    public void removeItem(SlideCutListView.RemoveDirection direction, int position) {
        adapter.remove(adapter.getItem(position));
        switch (direction) {
            case RIGHT:
                Toast.makeText(this, "向右删除  "+ position, Toast.LENGTH_SHORT).show();
                break;
            case LEFT:
                Toast.makeText(this, "向左删除  "+ position, Toast.LENGTH_SHORT).show();
                break;

            default:
                break;
        }

    }
}


ViewDragHelper

官方解释

ViewDragHelper是一个用于编写自定义ViewGroups的实用程序类。 它提供了许多有用的操作和状态跟踪,允许用户在其父ViewGroup中拖动和重新定位视图。

常量

int DIRECTION_ALL

表示沿所有轴进行检查

int DIRECTION_HORIZONTAL

表示检查应沿水平轴进行

int DIRECTION_VERTICAL

表示检查应沿垂直轴进行

int EDGE_ALL

边缘标志集指示所有边缘都应受影响。

int EDGE_BOTTOM

指示底部边缘应受影响的边缘标志。

int EDGE_LEFT

指示左边缘应受影响的边缘标志。

int EDGE_RIGHT

边缘标志,指示右边缘应受影响。

int EDGE_TOP

边缘标志,指示上边缘应受影响。

int INVALID_POINTER

空/无效指针ID。

int STATE_DRAGGING

目前正在拖曳视图。

int STATE_IDLE

由于fling / snap,当前没有拖动或动画视图。

int STATE_SETTLING

视图作为飞行或预定义的非交互式运动的结果目前正在固定到位。

Public methods

void abort()

cancel(),而且中止正在进行的所有运动并捕捉到任何动画的结束。

void cancel()

调用此方法的结果相当于process Touch Event(android.view.MotionEvent)接收ACTION_CANCEL事件。

void captureChildView(View childView, int activePointerId)

捕获特定的子视图以在父对象中拖动。

boolean checkTouchSlop(int directions, int pointerId)

检查在当前手势中跟踪的指定指针是否已经超过所需的斜率阈值。

boolean checkTouchSlop(int directions)

检查在当前手势中跟踪的任何指针是否已经越过所需的斜率阈值。

boolean continueSettling(boolean deferCallbacks)

将捕获的建立视图移动当前时间的适当量。

staticViewDragHelper create(ViewGroup forParent, float sensitivity, ViewDragHelper.Callback cb)

Factory方法创建一个新的ViewDragHelper。

staticViewDragHelper create(ViewGroup forParent, ViewDragHelper.Callback cb)

Factory方法创建一个新的ViewDragHelper。

View findTopChildUnder(int x, int y)

在父视图的坐标系内找到给定点下的最顶层子项。

void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)

基于标准自由移动甩水行为设置捕获的视图。

int getActivePointerId()
View getCapturedView()
int getEdgeSize()

返回边的大小。

float getMinVelocity()

返回当前配置的最小速度。

int getTouchSlop()
int getViewDragState()

检索此帮助器的当前拖动状态。

boolean isCapturedViewUnder(int x, int y)

确定当前捕获的视图是否在父视图的坐标系中的给定点下。

boolean isEdgeTouched(int edges)

检查指定的任何边是否最初在当前活动的手势中被触摸。

boolean isEdgeTouched(int edges, int pointerId)

检查指定的边是否最初被指定的ID触摸了指针。

boolean isPointerDown(int pointerId)

检查给定的指针ID是否表示当前关闭的指针(根据ViewDragHelper的知识)。

boolean isViewUnder(View view, int x, int y)

确定提供的视图是否在父视图的坐标系中的给定点下。

void processTouchEvent(MotionEvent ev)

处理父视图接收的触摸事件。

void setEdgeTrackingEnabled(int edgeFlags)

对父视图的所选边缘启用边缘跟踪。

void setMinVelocity(float minVel)

设置将被检测为具有大于零的幅度(以像素每秒为单位)的最小速度。

boolean settleCapturedViewAt(int finalLeft, int finalTop)

在给定(左,顶)位置处设置捕获的视图。

boolean shouldInterceptTouchEvent(MotionEvent ev)

检查提供给父视图的onInterceptTouchEvent的此事件是否应导致父级拦截触摸事件流。

boolean smoothSlideViewTo(View child, int finalLeft, int finalTop)

将视图子视图动画化为给定(左,顶)位置。

ViewDragHelper好的特点
       ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁(这个view一般是指拥子view的容器即parentView)
       ViewDragHelper可以检测到是否触及到边缘
       ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中想办法;
       ViewDragHelper的本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置( 通过       offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在触摸的时候判断当前拖动的是哪个子View;
虽然ViewDragHelper的实例方法 ViewDragHelper.create(ViewGroup forParent, Callback cb) 可以指定一个被ViewDragHelper处理拖动事件的对象 ,但ViewDragHelper类的设计决定了其适用于被包含在一个自定义ViewGroup之中,而不是对任意一个布局上的视图容器使用ViewDragHelper。
想对ViewDragHelper更多了解,可以 参考这边博文

基本用法

public class VDHLayout extends LinearLayout{
    private ViewDragHelper viewDragHelper;
    private View mDragView;
    private View mAutoBackView;
    private View mEdgeTrackerView;
    private Point mAutoBackOriginPos = new Point();

    public VDHLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        //第二个参数就是滑动灵敏度【1.0f是正常的,较大的值更敏感。】
        viewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {

            //这个地方实际上函数的返回值为true则代表可以滑动,为false则不能滑动
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                //mEdgeTrackerView禁止直接移动
                return child == mDragView || child == mAutoBackView;
            }

            /**
             * 处理水平滑动。如果要实现水平方向滑动效果,该方法必须重写。因为它的默认值为0,即不发生滑动。
             * 通常情况下,只需返回left即可,当但需要更加精确地计算padding等属性的时候,就需要对left进行处理,
             * 并返回合适大小的值。
             * @param child
             * @param left 水平方向上child移动的距离
             * @param dx 比较前一次的增量
             * @return
             */
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return left;
            }

            /**
             * 处理垂直滑动。如果要实现垂直方向滑动效果,该方法必须重写。因为它的默认值为0,即不发生滑动。
             * @param child
             * @param top 垂直方向上child移动的距离
             * @param dy 比较前一次的增量
             * @return
             */
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return top;
            }

            /**
             * 该方法拖动结束后调用,通过重写该方法,可以非常简单地实现当手指离开屏幕后要实现的操作,这个方法内部是
             * 通过Scroller类来实现的,这也是重写computeScroll()方法的原因
             * @param releasedChild
             * @param xvel
             * @param yvel
             */
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                //mAutoBackView手指释放是可以自动回去
                if (releasedChild == mAutoBackView){
                    viewDragHelper.settleCapturedViewAt(mAutoBackOriginPos.x,mAutoBackOriginPos.y);
                    invalidate();
                }
            }

            //在边界拖动是回调
            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId) {
                viewDragHelper.captureChildView(mEdgeTrackerView,pointerId);
            }
        });
        viewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //将触摸事件传递给ViewDragHelper,此操作必不可少
        return viewDragHelper.shouldInterceptTouchEvent(ev);
    }

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mAutoBackOriginPos.x = mAutoBackView.getLeft();
        mAutoBackOriginPos.y = mAutoBackView.getTop();
    }

    //在所用的子View完成后调用该方法
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mDragView = getChildAt(0);
        mAutoBackView = getChildAt(1);
        mEdgeTrackerView = getChildAt(2);
    }
}
xml

<RelativeLayout 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:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <com.hh.person.customview.widget.VDHLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="#f00"/>

        <TextView
            android:layout_marginTop="10dp"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="#0f0"/>

        <TextView
            android:layout_marginTop="10dp"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="#00f"/>
    </com.hh.person.customview.widget.VDHLayout>
</RelativeLayout>



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值