Android进阶 View事件体系(一):概要介绍和实现View的滑动

news2024/9/21 2:50:10

Android进阶 View事件体系(一):概要介绍和实现View的滑动

在这里插入图片描述

内容概要

本篇文章为总结View事件体系的第一篇文章,将介绍的内容主要有:

  1. 什么是View和ViewGroup
  2. Android中View的坐标轴
  3. 手势检测和速度检测
  4. 如何实现View的滑动

Android中的View与ViewGroup

在更加深入地了解Android中View的事件体系之前,我们需要了解究竟什么是Android中的View。
在Android系统中,View类是继承于Object,用于构建用户界面的基本块之一。从官网来说:

This class represents the basic building block for user interface components. A View occupies a rectangular area on the screen and is responsible for drawing and event handling. View is the base class for widgets, which are used to create interactive UI components (buttons, text fields, etc.). The ViewGroup subclass is the base class for layouts, which are invisible containers that hold other Views (or other ViewGroups) and define their layout properties.

翻译过来就是:
这个类代表用户界面组件的基本构建块。View在屏幕上占据一个矩形区域,并负责绘制和事件处理。View是小部件(widgets)的基类,用于创建交互式的用户界面组件(按钮、文本字段等)。ViewGroup子类是布局(layouts)的基类,它们是不可见的容器,可以容纳其他的View(或其他ViewGroup),并定义它们的布局属性。

上面是比较官方的解释,实际上View就是Android中所有控件的基类,所以说,View是一种界面层的控件的一种抽象,其他所有的控件(比如:TextView,Button等)都是继承于View这个基类,这就会使所有的控件都会遵循一定的规则,我们也可以把这种规则理解为View的体系。

除此之外,还提到到了ViewGroup,ViewGroup就是容纳View的容器,一个ViewGroup中可以放置多个View,同时,ViewGroup自身也继承于View。ViewGroup作为View或者ViewGroup这些组件的容器,派生了多种布局控件子类,比如LinearLayout,RelativeLayout等。

在这里插入图片描述

如上图👆所示,就形成了一个View的树。一般我们都不会直接使用View或者ViewGroup,而是使用他们的派生类。不过,在我们了解完整个View的事件和体系之后,我们也可以拓展基类来实现我们想要的自定义View。

Android中的坐标系

如同所有其他系统一样,如果要描述一个实体在空间中的位置,无论是二维还是三维空间,都需要引入坐标系的概念。Android系统也不例外,在Android系统中存在两种坐标系,分别是Android坐标系View坐标系

Android坐标系(绝对坐标系)

在Android系统中,将屏幕左上角的顶点作为坐标系的原点,原点向右是X轴正方向,向下是Y轴正方向;在触控事件中,使用getRawX方法和getRawY方法获得的都是Android坐标系的坐标,Android坐标系也称为绝对坐标系。 具体如下图所示:
在这里插入图片描述

View坐标系(相对坐标系)

上面说到Android坐标系是绝对坐标系,那么View坐标系就可以称作是相对坐标系了。它与Android坐标系共同存在帮助我们控制控件。

View坐标系是描述View在其父ViewGroup中的位置的,主要有四个属性:top,left,right和bottom。top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标,并且这些坐标都是相对于View的ViewGroup来说的,即,以ViewGroup的左上角而非屏幕的左上角为原点,所以说View坐标系又称为相对坐标系,具体可以看下图👇:
在这里插入图片描述
在处理点击事件的时候,我们一般都是用的相对坐标系,这里也有一些方法可以获取相对坐标和绝对坐标,具体看下图所示:
在这里插入图片描述
getX和getY方法可以获取触摸点距离所在View的左边界和上边界的距离,也就是相对坐标系的值;
getRawX和getRawY方法可以获取触摸点的绝对坐标系值;
除此之外,View也有一些方法:

  • getTop():获取View自身顶边到其父布局顶边的距离;
  • getLeft():获取View自身左边到其父布局左边的距离;
  • getBottom():获取View自身底边到其父布局底边的距离;
  • getRight():获取View自身右边到其父布局右边的距离;

有了以上这些信息,我们也可以得出许多别的信息,比如我们可以View的宽和高:

width = getRight() - getLeft(); //View的宽度
height = getBottom() - getTop(); //View的高度

手势检测和速度检测

MotionEvent

在这一节中我们将要处理一些触摸事件,所以我们先来了解手指碰到屏幕后会产生哪些事件。首先先看MotionEvent,MotionEvent中定义了一系列手指触摸到屏幕后所产生的一系列事件,主要有以下:

事件含义
ACTION_DOWN表示手指按下屏幕时触发的事件。它是触摸事件序列的第一个事件。
ACTION_MOVE表示手指在屏幕上滑动时触发的事件。在手指滑动期间,会多次触发该事件。
ACTION_UP表示手指从屏幕上抬起时触发的事件。它是触摸事件序列的最后一个事件。
ACTION_CANCEL表示触摸事件序列被取消时触发的事件。在某些情况下,例如手指按下后突然有来电或系统通知,会导致触摸事件被取消。
ACTION_POINTER_DOWN当有多个手指同时按下屏幕时,除第一个手指外的其他手指按下时触发的事件。
ACTION_POINTER_UP当有多个手指同时按下屏幕时,除最后一个手指外的其他手指抬起时触发的事件。
ACTION_HOVER_MOVE当没有手指触摸屏幕,但有物体(如手指悬停、笔等)接近屏幕时触发的事件。
ACTION_BUTTON_PRESS代表鼠标按键按下的事件。它是指在连接到Android设备的鼠标设备上,当按下鼠标按钮时触发的事件。

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,比如说:

  • 点击屏幕后离开松手,事件序列为DOWN -> UP;
  • 点击屏幕后滑动一会再松手,事件序列DOWN -> MOVE -> -> UP;

在后面我们实现View跟手移动的时候就需要在onTouchEvent中处理这些事件。

TouchSlop

TouchSlop是系统所能够识别出的被认为是滑动的最小距离,也可以说是能触发MOVE事件的一个距离单元。换句话说,当手指在屏幕上滑动时,如果距离小于TouchSlop,那么系统将不会触发MOVE事件。TouchSlop是一个常量,一般与设备有关,我们可以通过getScaledTouSlop方法获取,返回值为int值:

//在Activity的onCreate方法中获取最小滑动值
int mindp = ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();

手势检测和速度检测

速度跟踪

速度跟踪就是用于追踪手指在滑动过程中的速度,包括水平和竖直方向上的速度。这里我们需要用到VelocityTracker类,顾名思义,这个类就是用来帮助我们进行速度跟踪的。我们可以在我们自定的View中滑动并检测滑动速度。

这里我们首次提到了自定义View,其实实现自定义View很简单,只需要新建一个类继承View然后实现其构造方法就行了,这里我们在自定义View的onTouchEvent方法中实现速度检测。上面提到过,手指触摸到手机屏幕后就会产生一系列事件,在onTouchEvent方法中我们就可以处理这些事件,这里给出整个自定义View的代码:

public class VelocityTrackerView extends View {
    private static final String TAG = "VelocityTrackerView";
    VelocityTracker velocityTracker;

    public VelocityTrackerView(Context context) {
        super(context);
    }

    public VelocityTrackerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public VelocityTrackerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public VelocityTrackerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                velocityTracker = VelocityTracker.obtain();//获取速度追踪对象
                break;
            case MotionEvent.ACTION_MOVE:
                velocityTracker.addMovement(event);//添加移动事件
                velocityTracker.computeCurrentVelocity(1000);//计算1000ms内的平均速度
                int xVelocity = (int) velocityTracker.getXVelocity(); //获取XY的速度值
                int yVelocity = (int) velocityTracker.getYVelocity();
                Log.d(TAG, "xVelocity is : "+ xVelocity);
                Log.d(TAG, "yVelocity is : "+ yVelocity);
                break;
            case MotionEvent.ACTION_UP:
                velocityTracker.clear();//清空速度追踪器
                velocityTracker.recycle();//回收速度追踪器
                break;
            default:
                break;
        }
        return true;
    }
}

可以发现,使用VelocityTracker主要分为以下步骤:

  1. 获取VelocityTracker对象:
 	velocityTracker = VelocityTracker.obtain();//获取速度追踪对象
  1. 添加需要追踪的移动事件组:
	velocityTracker.addMovement(event);
  1. 计算并且获取速度值:
	velocityTracker.computeCurrentVelocity(1000);//计算1000ms内的平均速度
    int xVelocity = (int) velocityTracker.getXVelocity();
    int yVelocity = (int) velocityTracker.getYVelocity();

这里的computeCurrentVelocity中传入的是时间间隔,单位为毫秒,这里传入1000即计算每一秒手指移动的像素数。需要说明的是,在获取X和Y轴的移动速度前需要先调用computeCurrentVelocity方法。另外,根据坐标系的定义,获取的速度也有正有负,正说明往坐标轴的正方向移动,同理负就说明往坐标轴的负方向移动。速度的计算可由以下公式表示:

  速度 = (终点位置 - 起点位置)/ 时间间隔;
  1. 重置并回收速度检测器
    velocityTracker.clear(); //重置速度检测器
    velocityTracker.recycle(); //回收速度检测器的内存

最后,在我们的布局文件中添加我们的自定义View就可以使用了(这里用的是约束布局):

 <com.example.zidingyi.VelocityTrackerView
            android:id="@+id/mVelocityTrackerView"
            android:layout_width="418dp"
            android:layout_height="265dp"
            android:layout_marginTop="100dp"
            android:background="#E3F2FD"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

手势检测

手势检测,是用于辅助检测用户的单机,滑动,长按,双击等行为。这里需要用到GestureDetector,这个和VelocityTracker异曲同工,相对来说使用比较简单。

首先我们要创建一个GestureDetector对象并且实现OnGestureListener接口,根据需要我们还可以实现OnDoubleTapListener从而能够监听双击行为:

	gestureDetector = new GestureDetector(getContext(),this);
	gestureDetector.setIsLongpressEnabled(false);//解决长按屏幕后无法拖动的现象

然后,可以在onTouchEvent中直接托管View的触摸事件:

	boolean consume = gestureDetector.onTouchEvent(event);
	return consume; 

下面给出完整的代码:

public class GestureDetectorView extends View implements GestureDetector.OnGestureListener {
    private static final String TAG = "GestureDetectorView";
    GestureDetector gestureDetector = null;

    public GestureDetectorView(Context context) {
        super(context);
    }

    public GestureDetectorView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public GestureDetectorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public GestureDetectorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(gestureDetector == null){
            gestureDetector = new GestureDetector(getContext(),this);
            gestureDetector.setIsLongpressEnabled(false);
        }
        boolean isConsume = gestureDetector.onTouchEvent(event);//接管onTouchEvent方法
        return isConsume;
    }


    @Override
    public boolean onDown(@NonNull MotionEvent e) {
        Log.d(TAG, "onDown: ");
        return true;
    }

    @Override
    public void onShowPress(@NonNull MotionEvent e) {
        Log.d(TAG, "onShowPress: ");
    }

    @Override
    public boolean onSingleTapUp(@NonNull MotionEvent e) {
        Log.d(TAG, "onSingleTapUp: ");
        return true;
    }

    @Override
    public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
        Log.d(TAG, "onScroll: ");
        return true;
    }

    @Override
    public void onLongPress(@NonNull MotionEvent e) {
        Log.d(TAG, "onLongPress: ");
    }

    @Override
    public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
        Log.d(TAG, "onFling: ");
        return true;
    }
}

其中提到的回调方法以及其意义总结在下表了:

方法名描述所属接口
onDown手指轻轻触摸屏幕的一瞬间OnGestureListener
onShowPress手指轻轻触摸屏幕,尚未松开或者拖动,由一个ACTION_DOWN触发 *需要和onDown区别,它强调的是没有松开或者拖动的状态OnGestureListener
onSingleTapUp手指(轻轻触摸屏幕后)松开,伴随这一个ACTION_UP而触发,这是单击行为OnGestureListener
onScroll手指按下屏幕并拖动,由一个ACTION_DOWN,多个ACTION_MOVE触发,这是拖动行为OnGestureListener
onLongPress手指长按屏幕OnGestureListener
onFling按下触摸屏,快速滑动后松开,由一个ACTION_DOWN,多个ACTION_MOVE和一个ACTION_UP触发,是快速滑动行为
onDoubleTap双击,由两次连续的单击组成,不可能和onSingleTapConfirmed共存onDoubleTapListener
onSingleTapConfirm严格的单击行为 *注意它和onSingleTapUp的区别,如果触发了onSingleTapConfirm,那么后面不可能再紧跟着另一个单机型为,即这只可能是单击,而不可能是双击中的一次单击onDoubleTapListener
onDoubleTapEvent表示发生了双击行为,在双击的期间,ACTION_DOWN,ACTION_MOVE和ACTION_UP都会触发此回调onDoubleTapListener

实现View的滑动

移动View的几种方式

1.scrollTo/scrollBy

scrollTo(x,y)表示移动到一个具体的坐标点,而scrollBy(dx,dy)则表示要移动的增量为dx和dy。其中,scrollBy最终也是要调用到scrollTo方法的:

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }
    
    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();
            }
        }
    }

需要注意的是,scrollTo和scrollBy方法移动的是整块画布之上的手机屏幕。什么叫整块画布呢?假设我们正用放大镜在看报纸,放大镜用来显示报纸上的内容。同样我们可以把放大镜看作是我们的手机屏幕,他们都用用来显示内容的。而报纸可以看做是屏幕下的画布,它们都是用来提供内容的。放大镜外的内容,也就是报纸的内容不会随着放大镜的移动而消失,他一直存在。

同样,我们的手机屏幕看不到的视图并不代表其不存在。可以先如图所示:
在这里插入图片描述
画布上有三个按钮控件,以手机屏幕左上角为原点,能被我们看到的控件只有位于60,60的按钮控件,其他两个按钮控件都是不可见的。假如我们调用了scrollBy(50,50)方法后将会显示什么呢?如果按照我们之前的思维的话,应该是button向X和Y的正方向轴均移动50个单位,之前位于(60,60)的button应该会位于(110,110)是吗?事实并非如此,由于scrollBy移动的是手机屏幕,所以移动完之后的情况应该是这样的:
在这里插入图片描述
由于整块手机屏幕被向X和Y轴的正方向移动了50个单位,所以正确的坐标应该是(10,10).所以说我们在用scrollBy或者scrollTo方法将View进行移动时,就需要将目标值加上负号:

	((View)getParent()).scrollBy(-offsetX,-offsetY)

2.修改布局参数

第二种实现移动View的方法就是重新修改并设置布局参数,这是一种相对比较灵活并且比较推荐的方法。LayoutParams(布局参数)中存储了View的布局参数,我们可以修改并且重新设置布局参数达到移动View的效果。比如我们想把一个Button向右平移100px,只需要将LayoutParams里的marginLeft参数增加100px即可。

不过在获取布局参数的时候,我们一定要记得把其转化成对应父布局的布局参数,比如说,如果我的父布局是ConstraintLayout,那么我们在获取布局参数的时候就需要这样写:

ConstraintLayout.LayoutParams params =
                        (ConstraintLayout.LayoutParams)getLayoutParams();

接着我们修改布局参数并重新设置布局参数:

params.leftMargin += offsetX;
params.topMargin += offsetY;
setLayoutParams(params);

这样就可以将view向右移动offsetX值,向下移动offsetY值。

3.使用动画

动画又可以分为普通动画和属性动画。这两者的区别是,普通动画并不能改变View的位置参数,只是将View的影响实现了移动,这样讲可能有一点抽象,具体来说就是,如果对一个Button向右平移了300个像素,我们点击移动后的Button并不会触发点击事件,而点击Button的原始位置却会触发点击事件。

那我们先来看如何实现普通动画,我们先在res目录中新建anim文件夹并创建translate.xml文件来定义动画的效果:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
    android:fillAfter="true">
    <translate
    	android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300"
        />
</set>

translate标签中的duration指的是动画的持续时间,单位为ms,这里指定的动画时间就是1s;后面的fromXDelta就是X坐标的起始位,toXDelta就是X坐标的终点位。至于最上面的fillAfter,是设置动画完成后View时候保持在动画结束后的位置,若没有设置为true,那么动画完成后View将返回原位。
接着我们对View加载动画:

mView.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate));

接下来我们来看属性动画,其和普通动画的区别就是它可以改变View的位置参数,这里我们不详细讲属性动画,简单介绍一下属性动画,首先我们需要获取到一个属性动画对象:

ObjectAnimator am1 = ObjectAnimator.ofFloat(customView,"translationX",0.f,200.0f,0f);

ObjectAnimator.ofFloat()方法用于创建一个浮点数值的属性动画。

  • customView:要执行动画的目标视图对象。
  • “translationX”:指定要进行动画操作的属性名称,这里是translationX,表示视图在X轴上的平移。
  • 0.f:动画的起始值,这里表示X轴平移的起始位置。
  • 200.0f:动画的中间值,这里表示X轴平移的结束位置。
  • 0f:动画的结束值,这里表示X轴平移的回到起始位置。

然后设置动画时间并启动动画:

am1.setDuration(1000).start();

这样就实现了平移过去再回来的效果。

4.layout方法

第四种方法就是直接使用layout方法,layout()方法是View类的一个方法,它接受四个整数参数:left、top、right和bottom,用于指定视图在父容器中的位置。这些参数表示视图的左上角和右下角在父容器坐标系中的坐标。

**不过并不太推荐这种方法,因为layout方法并不会改变View的布局参数。**当在其他地方设置布局参数时,就会出现视图位置跳变的情况。具体如果想在View中调用:

layout(getLeft()+offsetX,getTop()+offsetY,
		getRight()+offsetX,getBottom()+offsetY);

这样就实现了View的移动,不过还是要强调一下,不推荐这种方法。

实现弹性滑动

接下来我们来实现一个有实际意义的功能:实现弹性滑动。这里我们使用弹性滑动对象Scroller实现,主要是介绍Scroller实现弹性滑动的典型用法,先给出典型的流程:
在这里插入图片描述
首先我们需要获得一个弹性滑动对象:

Scroller mScroller = new Scroller(context);//初始化Scroller

然后我们需要重写View的computeScroll函数,这是一个回调方法,会在绘制View的时候在draw方法中调用该方法。在这个方法中我们调用scrollTo方法并通过scroller对象不断获得当前View应该处于的位置,我的理解就是讲一段一段特别微小的动画组合起来实现弹性动画的效果:

	public void smoothScrollTo(int destX,int destY){ //自己写的方法--用于给外部调用实现弹性滑动
       int scrollX = getScrollX();
       int delta = destX-scrollX;
       mScroller.startScroll(scrollX,0,delta,0,2000);
       invalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        //用于计算滚动量,当返回值为true时,说明滚动尚未完成;否则说明滚动已经完成
        if(mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();//不断进行重新绘制
        }
    }

接下来我们就可以在Activity中调用该方法实现弹性滑动了:

mScrollerView.smoothScrollTo(-400,0);//向右平移400个像素

不过需要注意的是,这里移动的是整个手机屏幕(实际上相对来说就是移动了整块画布),所以屏幕上所有的内容都会产生平移。

实现View跟手移动

接下来我们实现拖动View,View跟手移动的效果。之前我们介绍了这么多移动View的方法,这里采取更加推荐的修改布局参数的方法进行移动。

首先在按下View时记录下View的起始坐标:

    public boolean onTouchEvent(MotionEvent motionEvent){
        //只要有触摸事件,该两行代码就会执行记录下坐标
        int x = (int) motionEvent.getX();//
        int y = (int) motionEvent.getY();//
        //手指的移动是快于绘图重新绘制的
        switch (motionEvent.getAction()){
            case MotionEvent.ACTION_DOWN://事件按下
                lastX = x;
                lastY = y;
                Log.d(TAG, "DOWN");
                break;
 			...
    }

lastX 和 lastY参数是View的成员变量,用于记录开始滑动时View的起始坐标,这里用相对坐标系即可了。

接下来,我们在移动事件里记录下手指移动到的坐标并且计算出偏移量,最后修改布局参数不断移动View的位置:

 case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //这方法有问题  -->改进后的方法 将源代码中的 getLeft + offsetX 替换
                ConstraintLayout.LayoutParams layoutParams =
                        (ConstraintLayout.LayoutParams) getLayoutParams();//获取布局参数

                layoutParams.leftMargin = layoutParams.leftMargin + offsetX;
                layoutParams.topMargin = layoutParams.topMargin + offsetY;
                setLayoutParams(layoutParams);
                Log.d(TAG, "MOVE");
                break;

核心方法就是这两个,本质就是不断记录偏移量然后进行重绘,下面给出完整的View代码:

public class MyFloat extends View {
    private static final String TAG = "mView";
    int lastX ;
    int lastY ;
    public MyFloat(@NonNull Context context) {
        super(context);
    }


    public MyFloat(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyFloat(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    //使用布局参数LayoutParams
    public boolean onTouchEvent(MotionEvent motionEvent){
        //只要有触摸事件,该两行代码就会执行记录下坐标
        int x = (int) motionEvent.getX();//
        int y = (int) motionEvent.getY();//
        //手指的移动是快于绘图重新绘制的
        switch (motionEvent.getAction()){
            case MotionEvent.ACTION_DOWN://事件按下
                lastX = x;
                lastY = y;
                Log.d(TAG, "DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //这方法有问题  -->改进后的方法 将源代码中的 getLeft + offsetX 替换
                ConstraintLayout.LayoutParams layoutParams =
                        (ConstraintLayout.LayoutParams) getLayoutParams();//获取布局参数

                layoutParams.leftMargin = layoutParams.leftMargin + offsetX;
                layoutParams.topMargin = layoutParams.topMargin + offsetY;
                setLayoutParams(layoutParams);
                Log.d(TAG, "MOVE");
                break;
            case MotionEvent.ACTION_UP://事件抬起
                Log.d(TAG, "UP");
                break;
            case MotionEvent.ACTION_BUTTON_PRESS:
                break;
        }

        return true;//表示该事件已被处理并且消费掉了,不再传递给其他的视图
    }
}

这里介绍的是修改布局参数实现的,因为笔者觉得这种方法更具有广泛性且没有什么太大的缺点。使用其他的方式进行移动也可以实现,比如说layout方法,不过这可能会带来一些奇奇怪怪的问题。

  • 最后,附上github上的完整代码☞:完整代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/564945.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ZYNQ】ZYNQ7000 UART 控制器及驱动应用示例

UART 简介 我们在使用 PS 的时候&#xff0c;通常会添加 UART 控制器&#xff0c;用于打印信息和调试代码。除此之外&#xff0c;PS 在和外 部设备通信时&#xff0c;也会经常使用串口进行通信。 UART 控制器 UART 控制器是一个全双工异步收发控制器&#xff0c;ZYNQ 内部包…

ssm实现发送邮箱功能

参考&#xff1a;ssm整合JavaMail发送邮件_ssm整合mimemessage_ds_surk的博客-CSDN博客 我在这位前辈写的博客的基础上进行讲解完善&#xff0c;避免踩坑。 我的jdk版本&#xff1a;1.8.0_333 1、引入依赖 相信很多朋友都卡在这里&#xff1a; 1、没有JavaMailSenderImpl类 2、…

数字逻辑 期末

概述 教材&#xff1a;《电子技术基础&#xff08;数字部分&#xff09;》 第五版 7400系列是TTL型芯片&#xff0c;商用型 数制 十进制->二进制 除2取余法&乘2取整法&#xff08;注意精度&#xff0c;但计科简单不考&#xff09; 十六进制->二进制 一位变四位 八…

Linux下串口编程

Linux下串口编程 Linux下的串口编程是通过串口设备文件和串口通信的系统调用函数来实现的。Linux下的串口设备文件通常为/dev/ttyS或/dev/ttyUSB(*表示数字),这些设备文件代表了对应的串口硬件设备。 在进行串口编程之前,需要先打开并初始化串口设备,其中包括设置波特率…

Linux 在桌面添加快捷启动图标(可添加至收藏夹)

0 背景 在 Ubuntu 系统下启动程序一般在 Terminal 通过输入指令启动&#xff0c;如 ./cfw。对于常用的程序&#xff0c;为了方便&#xff0c;创建桌面快捷图标 .desktop。为了让图标能够添加在收藏栏中&#xff0c;将 .desktop 融入桌面环境。 1 创建 .desktop 文件 参考&…

dvwa靶场通关(一)

第一关&#xff1a;Brute force low 账号是admin&#xff0c;密码随便输入 用burp suite抓包 爆破得出密码为password 登录成功 Medium 中级跟low级别基本一致&#xff0c;分析源代码我们发现medium采用了符号转义&#xff0c;一定程度上防止了sql注入&#xff0c;采用暴力破…

如何成为一名黑客?小白必学的12个基本步骤

黑客攻防是一个极具魅力的技术领域&#xff0c;但成为一名黑客毫无疑问也并不容易。你必须拥有对新技术的好奇心和积极的学习态度&#xff0c;具备很深的计算机系统、编程语言和操作系统知识&#xff0c;并乐意不断地去学习和进步。 如果你想成为一名优秀的黑客&#xff0c;下…

大项目参考地址​编辑 大项目接口实现

目录 大项目参考地址​编辑 口语考试 纸笔口语考试通常会安排在笔试前一周至笔试后一周的任意一天&#xff0c;机考口语考试通常会安排在笔试当天或者与笔试日期尽可能相邻的日期。根据考务安排的需要&#xff0c;在特殊情况下&#xff0c;口试日期有可能超出此区间&#xff0…

Java——《面试题——多线程并发篇》

前文 java——《面试题——基础篇》 Java——《面试题——JVM篇》 目录 前文 1、说说Java中实现多线程有几种方法 2、如何停止一个正在运行的线程 3、notify()和notifyAll()有什么区别&#xff1f; 4、sleep()和wait() 有什么区别&#xff1f; 5、volatile 是什么?可…

nodejs+vue网络课程在线考试系统an7ib

在线考试系统的设计与实现主要实现角色有管理员和用户,管理员在后台管理学生模块、用户表模块、token表模块、考试资讯模块、考试记录表模块、试题表模块、试卷表模块、配置文件模块、在线答疑模块 采用了Windows10操作系统平台&#xff0c;使用vue前端模板node作为后台监控&am…

k8s补充+helm(待续)

目录 master高可用架构master节点——整个集群的控制中枢node节点——工作节点搭建kubeadm搭建二进制搭建 探针检测方式探针检查参数配置执行顺序为什么有了livenessProbe和readnessProbe还要有StartupProbe&#xff08;1.16&#xff09; 零宕机发布pod退出流程preStop 无状态服…

上网速度太慢?这样设置可以提升60%的上网速度!

虽然现在光纤上网是最好的上网方式&#xff0c;但是对于一般人的选择还是宽带上网&#xff0c;网速永远都是一个值得讨论的话题。花了那么多的钱&#xff0c;却得到的是低品质的网速服务&#xff0c;因此越来越多的人想方设法在现有条件上提高网速。网上的那些方法基本人人都会…

手把手教你用Python编写配置脚本引擎(福利篇)

版权声明&#xff1a;原创不易&#xff0c;本文禁止抄袭、转载需附上链接&#xff0c;侵权必究&#xff01; 目录 一、配置信息写入二、读取配置信息三、修改配置信息四、配置引擎总结五、作者Info 一、配置信息写入 配置信息初始化 定义配置引擎类和初始化方法&#xff0c;其…

day18文件上传下载与三层架构思想

servlet文件上传 注意事项:在写了响应后,若后面还需要执行代码,需要添加return; apach的servlet3.0提供了文件上传的功能. **在客户端中的jsp如何上传文件:**使用form标签 使用input标签type的file属性 form表单中的的enctype必须加:使用二进制的方式进行传输,否则不能进行…

day20 过滤器和监听器

过滤器Filter 作用:对请求和响应进行预处理 使用场景:字符编码处理,登录检验,敏感词过滤,前端框架分发器 Filter的开发步骤 filter也是一个web组件,结构和servlet相似 1.定义类:实现javax.servlet.Filter接口 2.覆盖里面的3个方法: innit:初始化 doFilter:对请求和响应…

线程池C和C++实现

一、线程池介绍 1&#xff09;应用场景 当并发数很多的时候&#xff0c;并且每个线程执行时间很短的任务&#xff0c;这样就会频繁创建线程&#xff0c;而这样的频繁创建和销毁线程会大大降低系统的执行效率。对于这种场景我们可以使用线程池来复用之前创建的线程&#xff0c…

Java 与排序算法(7):堆排序

一、堆排序 堆排序是一种基于比较的排序算法&#xff0c;它的基本思想是将待排序的元素构建成一个堆&#xff0c;然后依次将堆顶元素取出&#xff0c;放到已排序的序列中&#xff0c;直到堆中所有元素都被取出&#xff0c;最终得到一个有序的序列。 堆是一种特殊的树形数据结…

5.24 基础题目

快速幂 #include<bits/stdc.h> using namespace std; //126348976 982638476 938420413 int main(){int a,b,p;cin>>a>>b>>p;long long res 1,ci1;int flag0;if(b0){res%p;}else{while(b){if (flag0)cia%p;elseci(ci%p)*(ci%p)%p;if (b&1)res(res…

【学习随笔】

2022/11/13 HTML :讲完了 css&#xff1a;讲完了 作业&#xff1a;编写登陆界面、整理一下sql优化,对于mybatis不熟练的继续练习 关于MySQL优化的问题? 思路总结&#xff1a;主要考虑数据库优化与SQL语句优化。 1&#xff0c;数据库优化&#xff0c;包括存储引擎的优化&…

FreeRTOS:时间管理

目录 前言一、FreeRTOS 延时函数1.1函数vTaskDelay()1.2 函数prvAddCurrentTaskToDelayedList()1.3 函数vTaskDelayUntil() 二、FreeRTOS 系统时钟节拍 前言 在使用FreeRTOS 的过程中我们通常会在一个任务函数中使用延时函数对这个任务延时&#xff0c;当执行延时函数的时候就…