Android :安卓学习笔记之 Android View 的基础知识和冲突事件处理

Android View

1、View的基础知识

在这里插入图片描述

1.1 什么是View

在Android中,什么是View?

  • View是Android中所有控件的基类,不管是简单的TextView,Button还是复杂的LinearLayout和ListView,它们的共同基类都是View;
  • View是一种界面层的控件的一种抽象,它代表了一个控件,除了View还有ViewGroup,从名字来看ViewGroup可以翻译为控件组,即一组View;
  • 在Android中,ViewGroup也继承了View,这就意味着View可以是单个控件,也可以是由多个控件组成的一组控件

1.2 View的位置参数

View和位置主要由它的四个顶点来决定,分别对应View的四个属性:top、left、right、bottom

  • top是左上角纵坐标
  • left是左上角横坐标
  • right是右下角横坐标
  • bottom是右下角纵坐标

对应如图所示:
在这里插入图片描述
根据上图我们可以得到View的宽高和坐标的关系;

width = right - left;
hight = bottom - top;

如何得到View的这四个参数呢?

left = getLeft();
right = getRight();
top = getTop();
bottom = getBottom();

注:从Android 3.0开始,View增加了几个额外的参数:x,y,translationX和translationY,

  • xy是View左上角的坐标
  • translationXtranslationY是View左上角相对于父容器的偏移量。

这几个参数也是相对于父容器的坐标;这几个参数的换算关系如下:

x = left + translationX;
y = top + translationY;

在这里插入图片描述
自定义View基础必知必会!

1.3 MotionEvent和TouchSlop

1.3.1. MotionEvent

在这里插入图片描述
此处需要特别说明:事件列,即指从手指接触屏幕至手指离开屏幕这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。
在这里插入图片描述

1.3.2.TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离

  • 当手指在屏幕上滑动的时候,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。
  • 这是一个常量,和设备有关,在不同设备上这个值可能是不同的

通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获取这个常量

在源码中还可以找到这个常量的定义

这个常量定义在frameworks/base/core/res/res/values/config.xml文件中,"config_viewConfigurationTouchSlop"对应的就是这个常量的定义。

1.4 VelocityTracker、GestureDetector和Scroller

1.4.1 VelocityTracker:速度追踪

用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。在View的onTouchEvent方法中追踪当前单击事件的速度:

VelocityTracker velocityTracker = VelocityTracker.obtain();
 velocityTracker.addMovement(event);

接着我们就可以来获取速度了,但是获取速度之前必须先计算速度:

velocityTracker.computeCurrentVelocity(1000);//在1000ms中的速度
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();

这里的速度是指一段时间内手指滑过的像素数,比如时间间隔设为1000ms时,在1s内,手指在水平方向从左向右滑过100像素,那么水平速度就是100。

  • 速度可能为负数,当手指从右向左滑动时,产生的速度就是负数,如果时间间隔是100ms,100ms内从左向右滑过100像素,那么速度就是100/0.1s = 1000像素。

速度 = (终点位置 - 起点位置)/ 时间段 ;

当不使用它的时候,需要调用clear方法来重置并回收内存:

        velocityTracker.clear();
        velocityTracker.recycle();

1.4.2 GestureDetector:手势检测

用于辅助检测用户的单击,滑动,长按,双击等行为。
GestureDetector的使用:

  • 首先需要创建一个GestureDetector对象并继承OnGestureListenerOnDoubleTapListener接口
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法手动的问题
mGestureDetector.setIsLongpressEnabled(false);

  • 接管View的onTouchEvent方法,在待监听的View的onTouchEvent方法中添加如下实现:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

然后我们就可以有选择的实现这两个接口中的方法了:
在这里插入图片描述
在这里插入图片描述
在实际开发中,如果只是监听滑动相关的,建议在onTouchEvent中实现,如果是监听双击这种行为,使用GestureDetector。

1.4.3 Scroller:弹性滑动对象

弹性滑动对象,用于实现View的弹性滑动(实现滑动过程)

View中使用的scrollTo/scrollBy进行滑动时是瞬间完成的

Scroller需要与View的computeScroll方法结合后可以实现滑动过程

Scroller scroller = new Scroller(getContext());

    private void smoothScrollerTo(int destX, int destY){
    
    
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        scroller.startScroll(scrollX,0,delta,1000);
        invalidate();//重绘界面
    }

    @Override
    public void computeScroll() {
    
    
        if(scroller.computeScrollOffset()){
    
    
            scrollTo(scroller.getCurrX(),scroller.getCurrY());
            postInvalidate();
        }
    }

2、View的滑动

在Android设备上,滑动几乎是应用的标配,通过三种方法可以实现View的滑动:

  • 第一种通过View本身提供的ScrollTo/ScrollBy方法来实现滑动;
  • 第二种通过动画View施加平移效果来实现滑动;
  • 第三种通过改变View的LayoutParams使得View重新布局从而实现滑动。

2.1 使用scrollTo/scrollBy

/**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    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();
            }
        }
    }


 /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
    
    
        scrollTo(mScrollX + x, mScrollY + y);
    }
  • scrollBy实际也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo实现了基于所传递参数的绝对滑动。
  • 在滑动过程中,mScrollX的值总是等于View的左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总等于View的上边缘和View内容上边缘在竖直方向的距离
  • scrollToscrollBy只能改变View内容的位置而不能改变View在布局中的位置。

参数说明:

  • mScrollXmScrollY的单位为像素,并且当View左边缘在View内容左边缘的右边时,mScrollX为正值,反之为负值;
  • 当View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。
  • 换句话说,如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。
    在这里插入图片描述

2.2 使用动画

使用动画,主要就是操作View的translationXtranslationY属性,既可以采用View动画,也可以采用属性动画。

1.传统的View动画

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
//需要设置这个属性为true才会保留动画后的状态,否则会还原
    android:fillAfter="true"
    android:zAdjustment="normal" >

    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100" />

</set>

这个操作并不能真正改变View的位置参数,包括宽/高

2.采用属性动画

//按顺序对应括号内的参数(目标view,水平移动,0位置,到100位置,100ms内移动)
ObjectAnimation.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();

View动画是对View的内容做操作,它并不能真正改变View的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后其动画结果会消失,View会瞬间恢复到动画前的状态。使用属性动画不会存在上述问题。

2.3 改变布局参数

即改变LayoutParams

一、比如将一个Button右移100px:将这个Button的L ayoutParams 里的marginLeft参数的值增加100px

二、在Button左边放一个空的View,然后设置这个View的宽度来挤动Button(意思这个改变参数布局的可以灵活使用)

例子:

MarginLayoutParams params = (MarginLayoutParams) mButton 1. getLayoutParams ();
params.width += 100;
params.leftMargin += 100;
//下面是应用这个改动
mButton 1. requestLayout ();
//  mButton 1. setLayoutParams (params);

2.4 各种滑动方式的对比

1、scrollTo/scrollBy:View提供的原生方法,可以比较方便地实现滑动效果并且不影响内部元素的单击事件。缺点:只能滑动View的内容,并不能滑动View本身。

2、动画:如果是Android3.0以上并采用属性动画,那么这种方式没有明显的缺点;如果是使用View动画或者在Android3.0以下使用属性动画,均不能改变View本身的属性。如果动画元素不需要响应用户的交互,那么可以用动画来做滑动,否则不太适合。一些复杂的效果必须通过动画才能实现。

3、改变布局:使用起来麻烦些,没有明显的缺点。适用于一些具有交互性的View。

2.4 .1 一个全屏滑动的例子

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    
    
		//获取当前手指的坐标
		//注意不能使用getX/Y,因为这是全屏滑动,所以需要获取当前点击事件在屏幕中的坐标而不是相对于View本身的坐标
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        switch (event.getAction()) {
    
    
        case MotionEvent.ACTION_DOWN: {
    
    
            break;
        }
        case MotionEvent.ACTION_MOVE: {
    
    
			//获得位移,这样才可以移动View
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
            int translationX = (int)ViewHelper.getTranslationX(this) + deltaX;
            int translationY = (int)ViewHelper.getTranslationY(this) + deltaY;
			//ViewHelper提供的动画效果
            ViewHelper.setTranslationX(this, translationX);
            ViewHelper.setTranslationY(this, translationY);
            break;
        }
        case MotionEvent.ACTION_UP: {
    
    
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

3 弹性滑动

3.1 使用Scroller

Scroller scroller = new Scroller(mContext);
    private void smoothScrollTo(int destX, int destY) {
    
    
        int scrollX = getScrollX();
        int delta = destX - srollX;
        //1000ms内滑向destX,效果是慢慢滑动
        mScroller.startScroll(scrollX, 0, delta, 0, 1000);
		//由下面代码可知startScroll只是单纯的保存了参数
		//invalidarte方法会导致View重绘,然后View中的draw方法(后面会提到)会调用computeScroll方法
		invalidarte();
    }
	
	 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    
    
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }
	
	//这个方法原本是空的,以下是为了实现弹性滑动而写的
	    @Override
    public void computeScroll() {
    
    
        if (mScroller.computeScrollOffset()) {
    
    
			//向Scroller获取当前的scrollX/Y,通过scrollTo实现滑动
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
			//使用这个方法进行第二次重绘,再调用computeScroll,一直反复到滑动过程结束
            postInvalidate();
        }
    }

再看一下computeScrollOffset的实现

    /**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    public boolean computeScrollOffset() {
    
    
        if (mFinished) {
    
    
            return false;
        }

		//由这个关键词可知这个方法是根据时间流逝来计算当前scrollX/Y的值
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
    
    
            switch (mMode) {
    
    
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
		····
        }
		//返回这个表示滑动未结束
        return true;
    }

在这里插入图片描述

3.2 通过动画

//让一个view在100ms内向右滑动100像素
ObjectAnimatior.ofFloat(targetView,"translationX",0,100).setDuration(100).start;

同时我们还可以利用动画的特性来实现一些动画不能实现的效果

下面是模仿Scroller来实现View的弹性滑动

final int starX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimation.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimationUpdateListener({
    
    
	@Override
	public void onAnimationUpdate(ValueAnimation animator){
    
    
		//获取动画的每一帧和之前的Scroller类似
		float faction = animator.getAnimatedFraction();
		mButton1.scrollTo(startX + (int) (deltaX * fraction),0);
	}
});
animator.start();

使用这种方法还可以实现其他动画效果,只需要在onAnimationUpdate方法中加上我们需要的操作

3.3 使用延时策略

核心思想是通过发送一系列延时消息而达到的渐进式效果

  • 使用Handler或者View的postDelayed方法/线程的sleep方法

下面的代码是大约1000ms(因为线程调度不会很精确稳定)内将View的内容向左移动100像素

    private static final int MESSAGE_SCROLL_TO = 1;
    private static final int FRAME_COUNT = 30;
    private static final int DELAYED_TIME = 33;

    private Button mButton1;
    private View mButton2;

    private int mCount = 0;

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
    
    
        public void handleMessage(Message msg) {
    
    
            switch (msg.what) {
    
    
            case MESSAGE_SCROLL_TO: {
    
    
                mCount++;
                if (mCount <= FRAME_COUNT) {
    
    
                    float fraction = mCount / (float) FRAME_COUNT;
                    int scrollX = (int) (fraction * 100);
                    mButton1.scrollTo(scrollX, 0);
                    mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
                }
                break;
            }

            default:
                break;
            }
        };
    };

4 View的事件分发机制

Android :安卓学习笔记之 事件分发机制 的简单理解和使用

5 View的滑动冲突

一文解决Android View滑动冲突

5.1 常见的滑动冲突场景

常见滑动冲突场景可以简单分为如下三种:

  • 场景1–外部滑动方向和内部滑动方向不一致
常见的是ViewPager和Fragment结合实现页面左右滑动效果

然而每个页面中又有一个ListView上下滑动,因为ViewPager内部处理了这种滑动冲突,所以不会出现问题

如果我们使用的事ScrollView而不是ViewPager,那就必须手动处理冲突,否则只有其中一层可以滑动
  • 场景2–外部滑动方向和内部滑动方向一致
    • 系统无法知道用户到底想让哪一层滑动,要么只有一层滑动,要么两个都滑动的很缓慢
  • 场景3–上面两种情况的嵌套
    • 几乎就只是单一的滑动冲突的叠加,因此只需要分别处理内层、中层和外层之间的滑动冲突即可
      在这里插入图片描述

5.2 滑动冲突的处理规则

  • 对于场景1,根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。
  • 对于场景2,根据业务规则来决定由谁拦截事件。
  • 对于场景3,根据业务规则来决定由谁拦截事件。

在这里插入图片描述

5.3 滑动冲突的解决方式

5.3.1.外部拦截法

即父View根据需要对事件进行拦截。逻辑处理放在父View的onInterceptTouchEvent方法中。我们只需要重写父View的onInterceptTouchEvent方法,并根据逻辑需要做相应的拦截即可。

    public boolean onInterceptTouchEvent(MotionEvent event) {
    
    
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
    
    
            case MotionEvent.ACTION_DOWN: {
    
    
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
    
    
                if (满足父容器的拦截要求) {
    
    
                    intercepted = true;
                } else {
    
    
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
    
    
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

在这里插入图片描述

5.3.2.内部拦截法

即父View不拦截任何事件,所有事件都传递给子View,子View根据需要决定是自己消费事件还是给父View处理。这需要子View使用requestDisallowInterceptTouchEvent方法才能正常工作。下面是子View的dispatchTouchEvent方法的伪代码:

    public boolean dispatchTouchEvent(MotionEvent event) {
    
    
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
    
    
            case MotionEvent.ACTION_DOWN: {
    
    
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
    
    
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此类点击事件) {
    
    
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
    
    
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

父View需要重写onInterceptTouchEvent方法:

    public boolean onInterceptTouchEvent(MotionEvent event) {
    
    

        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
    
    
            return false;
        } else {
    
    
            return true;
        }
    }

在这里插入图片描述

参考

1、Android开发艺术探索——View的事件体系
2、Android开发艺术探索之View的事件体系

猜你喜欢

转载自blog.csdn.net/JMW1407/article/details/122405710