前言
本章是对Android基础知识中的View组件和机制进行复习
一、View基础概念
什么是View
View是Android所有控件的基类,类似Java中所有对象的基类Object;View是一种界面层的控件的一种抽象,它代表了一个控件。
ViewGroup是控件组,一个ViewGroup内部包含多个View,即一组View
所以一个View可以是一个控件或者是多个控件或一个控件组组成,这就形成了View树,类似于前端的DOM树的结构。
View的位置参数
原始位置参数
View的原始位置参数由四个属性决定:left、top、right和bottom;left和top是相对View的左上角而言的,right和bottom是相对View的右下角而言的。
相关方法
Left=getLeft();
Right=getRight();
Top=getTop();
Bottom=getBottom();
width=right-left;
height=bottom-top;
补充位置参数
除了以上的位置参数外,还有x、y、transactionX和transactionY四个参数,x、y是View左上角的坐标,transactionX和transactionY是View相对于父容器的偏移量。
当View进行滑动时,其原始位置参数是不会发生改变的,可能改变的是x、y、transactionX和transactionY这四个参数。
由公式如下:
x=left+transactionX;
y=top+transactionY;
View的接触事件
MotionEvent
这个事件的理解相对简单,对比Java的Swing事件处理即可
- ACTION_DOWN 手指刚接触屏幕
- ACTION_MOVE 手指在屏幕上移动
- ACTION_UP 手指从屏幕上松开的一瞬间
事件组成:
- 点击屏幕后离开松开 DOWN-UP
- 点击屏幕后滑动松开 DOWN-MOVE…-UP
相关方法
调用MotionEvent对象
getX/getY //是当前接触点相对于View左上角的位置
getRawX/getRawY //是当前接触点相对于手机屏幕左上角的位置
TouchSlop
这个是系统所能识别出的最小的滑动常量,即你的手指在屏幕上滑动时,如果两次滑动的距离小于这个常量,则系统不认为你在进行滑动操作
可调用方法
ViewConfiguration.get(getContext()).getScaledTouchSlop()
进行获取
VelocityTracker
这个类看类名便知道是在进行速度追踪工作,它用来追踪手指在滑动过程中的速度,包括水平和竖直方向的速度,使用过程为在View的onTouchEvent方法中进行追踪当前事件的速度
VelocityTracker vt=VelocityTracker.obtain();
vt.addMovement(event);//绑定事件
vt.computeCurrentVelocity(1000);//先调用方法设置速度计算的时间间隔
int xV=(int)vt.getXVelocity();
int yV=(int)vt.getYVelocity();
计算的单位为像素,以x轴方向举例,在vt.computeCurrentVelocity(1000);设置时间间隔为1000毫秒的基础上,如果1000毫秒内向右移动了1000像素,则水平速度xV为1000,y轴同理
这里需要注明,速度可以有正负,正负表示是否沿着计算机屏幕规定的x轴、y轴正方向移动
需要注意的是,当不需要使用该对象时,可以主动调用回收方法进行回收
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
GestureDetector这个类主要用于接管和处理Android的手势检测,包括但不限于双击、滑动等事件
在 android 开发过程中,我们经常需要对一些手势,如:单击、双击、长按、滑动、缩放等,进行监测。这时也就引出了手势监测的概念,所谓的手势监测,说白了就是对于 GestureDetector 的用法的使用和注意要点的学习。我们通过传入 MotionEvents 对象,就可以在各种事件的回调方法中各种手势进行监测。举个例子: GestureDetector 的 OnGestureListener 就是一种回调方法,就是说在获得了传入的这个 MotionEvents 对象之后,进行了处理,我们通过重写了其中的各种方法(单击事件、双击事件等等),就可以监听到单击,双击,滑动等事件,然后直接在这些方法内部进行处理。
这里放一篇详细介绍的博客
GestureDector
其实手势识别现在的类很多啊,大家不一定要局限于只看官方提供的类
二、View的滑动
通常情况下,Android控件的滑动机制归根到底有三种基础机制
- 通过View本身scrollTo/scrollBy方法实现
- 通过动画给View添加效果
- 通过改变View的LayoutParams使得View重新布局进行滑动
调用scrollTo/scrollBy方法
scrollTo和scrollBy两个方法是Scroll类下面的方法,Scroller本身是不能实现View的滑动的,它需要和View的computeScroll方法配合才能发挥作用
这里给出在自定义View中使用Scroller的典型用法
Scroller mScroller = new Scroller(mContext);
private void smoothScroll(int destX, int destY) {
int scrollX = getScrollX();
int deltaX = destX - scrollX;
mScroller.startScroll(scrollX, 0, deltaX, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
scrollBy、scrollTo和startscroll的源码
/**
* 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);
}
/**
* 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();
}
}
}
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;
}
需要注意的是,使用scroll方法进行移动,只能将View的内容进行移动,而不能移动View在布局中的位置
mScrollX是View在布局中的位置的左边沿与View内容的左边沿相对于x轴正方向的偏移量
同理得到mScrollY
使用动画效果
package com.example.viewslidepractice.View;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
public class BaseOnAnimatorView extends View {
private int lastX;
private int lastY;
public BaseOnAnimatorView(Context context) {
super(context);
}
public BaseOnAnimatorView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BaseOnAnimatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BaseOnAnimatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean onTouchEvent(MotionEvent event){
int x=(int) event.getX();
int y=(int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX=x;
lastY=y;
break;
case MotionEvent.ACTION_MOVE:
//计算移动的距离,用于修正View移动后的位置
int offsetX=x-lastX;
int offsetY=y-lastY;
//放置View
layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
/**
* 方法二
* 基于offsetLeftAndRight&&offsetTopAndBottom方法进行坐标偏移修正
*/
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
break;
default:
break;
}
return true;
}
}
package com.example.viewslidepractice;
import androidx.appcompat.app.AppCompatActivity;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.os.Bundle;
import android.view.animation.AnimationUtils;
import com.example.viewslidepractice.View.BaseOnAnimatorView;
public class Animator_Activity extends AppCompatActivity {
private BaseOnAnimatorView baseOnAnimatorView;
private BaseOnAnimatorView baseOnAnimatorView_f;
private BaseOnAnimatorView baseOnAnimatorView_s;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_animator);
baseOnAnimatorView = new BaseOnAnimatorView(this);
baseOnAnimatorView_f = new BaseOnAnimatorView(this);
baseOnAnimatorView_s = new BaseOnAnimatorView(this);
baseOnAnimatorView =findViewById(R.id.ani_view);
baseOnAnimatorView_f = findViewById(R.id.ani_view_sec);
baseOnAnimatorView_s = findViewById(R.id.ani_view_third);
//设置动画,利用ObjectAnimator.ofFloat方法不仅可以移动View视图,还可以将响应区域移动过去
ObjectAnimator translationX
= ObjectAnimator.ofFloat(this.baseOnAnimatorView, "translationX", 0.0f, 200.0f, 0f);
ObjectAnimator scaleX
= ObjectAnimator.ofFloat(this.baseOnAnimatorView_f, "scaleX", 1.0f, 2.0f);
ObjectAnimator rotationX
= ObjectAnimator.ofFloat(this.baseOnAnimatorView_s, "rotationX", 0.0f, 90.0f, 0.0F);
/**
* 创建一个组合动画
*/
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(10000);
animatorSet.play(translationX)
.with(scaleX)
.after(rotationX);
animatorSet.start();
}
}
这里通过自定义View主要书写了利用Layout()放置属性实现View的移动放置,而后在Activity中创建一个动画类,包含水平移动、变换形象、旋转等操作,主要是通过ObjectAnimator和AnimatorSet两个类进行实现
改变布局参数
这里主要呈现通过改变LayoutParams参数进行View的移动
package com.example.viewslidepractice.View;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
public class BaseOnLayoutParamsView extends View {
private int lastX;
private int lastY;
public BaseOnLayoutParamsView(Context context) {
super(context);
}
public BaseOnLayoutParamsView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BaseOnLayoutParamsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BaseOnLayoutParamsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* 通过改变布局参数实现View的滑动
* @param event 点击事件
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event){
int x=(int) event.getX();
int y=(int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX=x;
lastY=y;
break;
case MotionEvent.ACTION_MOVE:
//计算移动的距离,用于修正View移动后的位置
int offsetX=x-lastX;
int offsetY=y-lastY;
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin=getLeft()+offsetX;
layoutParams.topMargin=getTop()+offsetY;
setLayoutParams(layoutParams);
break;
default:
break;
}
return true;
}
}
这里通过重写onTouchEvent()获取事件坐标,而后将差值设置进参数列表里面,而后最后传递参数列表给View
比较和总结
- scrollTo/scrollBy,原生方法操作简单,但是不能移动View的内容,因而移动过后包括点击事件等事件尚在原地,十分地不友好
- 操作简单,但是会影响交互,主要适用于不需要交互操作的View
- 改变布局参数:操作稍微复杂,但是在需要注重交互的场景比较有效
github练习项目地址
View的手指移动练习三种方式实现
三、View的事件分发机制
View有两大难点,一个为事件分发机制,另外一个为滑动冲突处理;但是后者的核心仍然为事件分发机制
什么是事件分发过程
当我们进行点击时,就要对这个点击事件进行分发,所谓点击事件的分发,就是对一个MotionEvent事件进行分发的过程,即当一个MotionEvent产生以后,系统把这个MotionEvent传递给具体的View,而这个传递过程就是事件分发的过程
事件分发方法
事件分发过程由三个方法组成
-
public boolean dispatchTouchEvent(MotionEvent ev)
该方法用来事件的分发,如果事件传递到当前View,则该方法一定会被调用,返回值受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否会消耗掉当前事件 -
public boolean onInterceptTouchEvent(MotionEvent event)
在dispatchTouchEvent方法内部调用,用来判断是否要要拦截某个事件,返回结果表示是否要拦截当前的事件 -
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否要消耗当前事件,如果不进行小号,则在同一个事件序列中,当前View无法再次接收到事件
他们之间的关系用一段伪码阐述最为合理
对于一个根ViewGroup,点击事件产生以后,首先会传递给它,而后根ViewGroup的dispatchTouchEvent调用,如果onInterceptTouchEvent方法返回True表明它要拦截该事件,而后该事件交给ViewGroup处理,这时候ViewGroup的onTouchEvent方法被调用;
注:ViewGroup默认不拦截任何事件
如果ViewGroup的onInterceptTouchEvent方法返回false,表示它不拦截当前事件,则事件会交给它的子View处理,接着子元素的dispatchTouchEvent被调用,如此返回递归直到事件被最终处理
事件处理的优先级
当一个View需要处理点击的事件,处理的顺序大致如下
- 有OnTouchListener,调用其中的onTouch方法,若onTouch方法返回值为false,则调用onTouchEvent方法
- 在OnTouchEvent方法中,若设置有OnClickListener,则调用其OnClick方法
优先级:OnTouchListener>OnTouchEvent>OnClickListener
事件传递顺序
当一个点击事件产生,传递顺序遵循如下规则:Activity->Window->View
事件总是先传递给Activity,然后传递给Window,而后Window传递给顶级View,顶级View按照事件分发机制去分发事件
若一个View的onTouchEvent返回false,则父容器的onTouchEvent会被调用
笔者的理解是,这个分发问题就好像是先一层一层往下传,然后如果最下面那层处理不了这个问题,那么就只能往回传,让父类去进行解决;
就好像公司里,总监把任务交给主管,主管把任务交给你的Mentor,Mentor把任务交给你,最后你因为能力不够解决不了,就得你的Mentor去解决,如果你的Mentor也解决不了,那只能再往上传
Android界面的层次关系
考虑到后面的源码解析部分要用到大量的Android界面层次关系的知识,这里就先进行一番复习
常见的层次就是Activity、Window、DecorView以及ViewRoot之间的层次关系
Activity
Activity并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View进行交互。
笔者的理解里,Activity更像是一个组织 or 注册的公司,囊括下面的一层层组织
Window
Window位于Activity层下面,是视图的承载器,但是Window类是抽象类,其本身不实现任何方法,我们通常所说的Android Window其实是实现了抽象类的PhoneWindow
Window内部持有一个 DecorView,而这个DecorView才是 view的根布局。Window是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局R.layout.activity_main。Window通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。
DecorView
DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。具体情况和Android版本及主体有关,以其中一个布局为例,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
我们常见的setContentView()方法,便是将ContentView布局扔入DecorView里面
ViewRoot
ViewRoot其实并不隶属于界面的组成,它更多的体现在作为中间层联系的方面;所有View的绘制以及事件分发等交互都是通过它来执行或传递的。
ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。
ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。
这里我们主要是了解每个部门各自扮演的角色,方便对事发分发机制的源码进行阅读,DecorView深化的部分我们后续继续深入阐述,现在继续往下看
事件分发的源码解析
上文已经提及,发生点击事件时,第一层进行转发的是Activity,那么我们先来看看Activity的分发部分
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//空方法,用于重写覆盖
}
if (getWindow().superDispatchTouchEvent(ev)) {//1
return true;
}
return onTouchEvent(ev);
}
Activity的分发工作具体是由Window来进行的,Window会将事件传递给DecorView,DecorView扮演的是当前界面的底层容器(即serContentView所设置的View的容器),Window可以通过Activity.getWindow.getDecorView()获得
上面的代码中,1处代码事件交给Window分发,如果返回true,则说明事件已经处理了了,则return true,否则调用onTouchEvent(ev);进行处理
接下来查看Window是如何将事件传递给ViewGroup的
众所周知,Window是个抽象类,我们得找到他的实现类,才能看到他的方法具体在做哪些工作
我们翻看Activity源码,找到了这些信息
@UnsupportedAppUsage
private Window mWindow;
//...
mWindow = new PhoneWindow(this, window,activityConfigCallback);
然后我们来看看Window实现类PhoneWindow的部分
public class PhoneWindow extends Window implements MenuBuilder.Callback{
...
@Override
public boolean superDispatchTrackballEvent(MotionEvent event) {
return mDecor.superDispatchTrackballEvent(event);
}
}
于是我们看到,PhoneWindow将事件直接传递给了DecorView,DecorView是继承自FrameLayout,而FrameLayout是继承自ViewGroup,所以走到这里,其实事件已经传递到了View这一层
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{
}
而后事件通过DecorView传递给顶级View,即在Activity中通过setContentView设置的View,顶级View一般为ViewGroup
顶级View对点击事件进行分发的过程
根据前文所述,当事件传递给ViewGroup时,ViewGroup会调用前文所提及的三个方法进行处理(要么拦截,要么交给子View,子View处理不了自己来试试),如此完成事件的分发。
这里看看ViewGroup中dispatchTouchEvent()的源码,因为篇幅过长,所有删除了一部分不在重点分析的源码,包括取消事件的源码分析
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
//一些清空操作,防止遗留的事件对后续操作产生影响
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);//将之前存储的Touch事件的目标View全部进行清除
resetTouchState();//每次ACTION_DOWN事件到来都会对标志位进行重置
}
//...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//mFirstTouchTarget是指向的可以处理该事件的子View
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//FLAG_DISALLOW_INTERCEPT标记位由requestDisallowInterceptTouchEvent方法生成
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//调用方法询问自己是否要进行拦截
ev.setAction(action); // restore action in case it was changed
//将action存储到MotionEvent对象中,与前文呼应,这里的作用在于,比方说ViewGroup决定对这个事件序列进行拦截,那么后续的所有事件序列,都交给他处理,而不在是调用onInterceptTouchEvent()进行判断
} else {
intercepted = false;//不进行拦截
}
} else {
//当事件不是初始的Down事件的时候,且没有TouchTarget时,直接对其进行拦截
//比方说ACTION_MOVE或者ACTION_UP这两个事件作用于ViewGroup上
intercepted = true;
}
//...
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}//进行拦截了,则目标无法获取到这个事件
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
这里是ViewGroup对于事件拦截的判断
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
当ViewGroup不进行点击事件拦截的时候,点击事件交由子View进行处理,这一部分的源码如下
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;//遍历子View,判断其是否能够收到点击事件:子元素是否在播动画&&落点是否位于子元素的xy边界内
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//将child作为参数传入dispatchTransformedTouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);//如果child返回true,那么对mFirstTouchEvent进行赋值,并且跳出子View的遍历循环,如果返回false,那么便遍历下一个子View
alreadyDispatchedToNewTouchTarget = true;
break;
}
这里
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);//调用父类的dispatchTouchEvent nonemethod
} else {
handled = child.dispatchTouchEvent(event);//这里调用子类的dispatchTouchEvent
}
event.setAction(oldAction);
return handled;
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
//对mFirstTouchTarget赋值,类似单链表的结构;如果mFirstTouchTarget没有被赋值,那么ViewGroup默认拦截接下来同一序列中的所有点击事件
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);//这里第三个参数为null,传到上文的nonemethod处,调用父类的方法,即调用View的方法
}
上述的分析表明,如果mFirstTouchTarget没有被赋值,那么ViewGroup会去调父类的dispatchTouchEvent()方法,即调View的dispatchTouchEvent()方法
以下是View部分的源码
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;//View的onTouchListener是否被实现,若实现,且能消耗掉,即使View不可用,仍然返回true,消费掉该事件
}
if (!result && onTouchEvent(event)) {
result = true;//调用自己的onTouchEvent()方法的返回值
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
来看OnTouchEvent()方法的重要部分
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;//如果支持众多clickable中的一个,那么后续必然有对其进行处理的步骤,反之返回false
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}//如果有代理机制,且代理机制能够处理这个事件,那么返回为true
至此,View的源码分发机制源码分析部分就完毕了,现在对其进行流程整理:
当一个ACTION_DOWN点击事件传递到ViewGroup时,会进行以下的操作
- 清空之前处理事件的目标View,重新设置标志位FLAG_DISALLOW_INTERCEPT,这个标志位由requestDisallowInterceptTouchMovement()方法决定,其向子类发出请求,告知自己是否需要拦截
- 根据标志位FLAG_DISALLOW_INTERCEPT和调用onInterceptTouchEvent()询问自己是否需要对事件进行拦截
- 如果不需要进行拦截且事件没有被取消,则遍历其子View集合,找到一个能够处理点击事件,并且坐标点满足要求,设置mFirstTouchTarget指向这个可以处理的子View,如果子View找不到可以处理的或者子View的onTouchEvent()返回了false,那么这时候事件重新回到ViewGroup手上,只能调用其父类View的onTouchEvent,如果父类onTouchEvent()中实现了任意一个点击接口,就消耗掉该事件,即使父类并不可用
- 如果进行了拦截,那么就没有上述寻找View的过程了,则对直接调用判断父类onTouchEvent()方法
那么从上述源码思考和理逻辑的过程中,我们可以这样做出陈述,ViewGroup()更像是一个只能分发,不能做事的主,有点击事件,拦截了交给View(父类),如果没有进行拦截,那么先交给儿子子类View,如果儿子们搞不定,重新交给上层的View(父类).
为了验证这条总结,我特意去创建了一个ViewGroup(),然后对其设置点击事件
结果如下:
点击setOnClickListener()方法
果然定位到了View层的onClickListener()方法
如此,View的分发机制便总结完毕啦!
四、如何处理滑动冲突
滑动冲突问题的产生
滑动冲突问题的产生,说白了就是一个界面内外两层的View都可以进行滑动,这个时候便会发生滑动冲突
常见的滑动冲突场景
- 内部和外部滑动方向不一致
- 内部和外部滑动方向一致
- 上述场景的组合
滑动冲突问题解决思路
外部拦截法
根据我们前文关于View的分发机制分析的部分,点击事件都需要事先经过父容器的拦截处理,那么根据之前滑动冲突的场景,我们便可以进行判断,如果父容器需要这个滑动,就拦截;如果不需要,则进行放弃,留给子View
伪码如下
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;
}
内部拦截法
内部拦截法则是统一让父容器不对任何事件进行拦截,根据子View的判断结果返回到父容器进行处理,这需要涉及requestDisallowInterceptTouchEvent()方法,对父容器能否进行拦截进行设置
伪码如下
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);
}
同时父容器不能拦截ACTION_DOWN事件,因为ACRTION_DOWN事件会重新设置标志位, 则parent.requestDisallowInterceptTouchEvent(true);这行代码设置的拦截标志位将毫无作用,且任何事件都会被拦截,所有事件无法传递到子View当中去
父元素进行如下修改
public boolean onInterceptTouchEvent(MotionEvent event){
int action =event.getAction();
if (action==MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
毫无疑问,相比较外部拦截法,内部拦截法需要进行两处设置,略显麻烦,因而优先推荐外部拦截法
这里提供一个利用外部拦截法解决第一类和第二类滑动冲突问题场景的DEMO
五、View的工作原理
ViewRoot和DecorView
ViewRoot
前文已经介绍过ViewRoot,这里对其再次进行阐述,但是这里要介绍View的三大流程,就不得不再对其进行提及
ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。
View的绘制流程是从ViewRoot的performTraversals方法开始,它经过measuer、layout和draw三大流程才将一个View绘制出来
具体如下:
如图,performTraversals依次调用performMeasure、performLayout和performDraw三大方法
以performMeasure为例,父容器依次调用performMeasure、measure、onMeasure方法,而后onMeasure方法中对子元素进行measure过程,紧接者子元素重复父容器的绘制方法,如此完成View的绘制
- measure过程决定View元素的宽高,getMeasureWidth和getMeasureHeight可获取元素经过Measure后的宽高
- layout过程决定View的四大顶点位置和实际宽高
- draw过程决定View的最终显示,只有完成了draw方法,View才能最终显示在屏幕上
DecorView
DecorView是一个顶级View,其包含的内容前面已经介绍过啦,一个LinearLayout里面包含如系统的ActionBar和APP自带的titlebar和内容栏(content),前者是ViewStub,后两者是FrameLayout,这里就不做赘述了
所有View层的事件,都先经过DecorView,再往下传递
View的三大流程
Measure
MeasureSpec
MeasureSpec是我们给View添加的LayoutParams和父容器施加的规则结合过后的参数,将最终决定View的实际宽高
MeasureSpec整体为32位整数,由两部分组成,高两位代表SpecMode,低的三十位代表SpecSize,这里Google用了一个精妙的打包和解包方法,有兴趣的小伙伴可以看看源码
SpecMode的三种状态如下:
DecorView的MeasureSpec由窗口大小和自身LayoutParams决定,普通View的Measure由窗口大小和自身LayoutParams决定
对于一个普通View的Measure过程,无非就是确定自身的MeasureSpec,然后调用Measure方法;但对于一个ViewGroup,除了完成自己的测量,还要完成其子元素的measure方法,子元素再递归调用其子元素的measure方法,如此最终完成。
对于普通View的measure流程,整理如下:
- measure()方法调用onMeasure()方法
- 调用getDefaultSize()结合specMode获取长和宽的数值
- 调用setMeasureDimension()方法给View设置长宽
对于ViewGroup的measure流程,整理如下:
- 调用measureChildren()方法,遍历children子元素View集合,依次调用measureChild()方法
- 获取子元素的参数LayoutParams(),而后根据与父元素的限制(包括父元素的内边距、父元素的大小)获取到childWidthMeasureSpec,传递给子元素的measure()方法
因为ViewGroup没有实现具体的测量方法,所以其本质还是根据比较和限制产生参数,然后调用子元素的measure()方法进行;之所以ViewGroup没有实现具体的onMeasure()方法,是因为不同的ViewGroup具备不同的测量特性,如LinearLayout和RelativeLayout测量的onMeasure()方法并不相同
因为View在绘制过程中可能会发生多次测量,所以在onMeasure()方法中去获取View的宽高可能并不是其实际的宽高,最终的宽高还得看onLayout()方法
Layout
Layout过程是ViewGroup用来确定其子View位置的过程,Layout过程涉及两个方法,layout()方法确定其View本身的位置,onLayout()方法确定其子元素的位置,具体流程如下:
- ViewGroup调用自身的onLayout()方法,其中遍历所有子元素并调用其layout()方法
- layout()方法中通过setFrame()方法确定View的四个顶点位置
- 确定顶点位置后View的onLayout()方法调用
- View的onLayout()方法调用setChildFrame()来为子元素设置特定的位置,根据竖直排列或是水平排列一层一层将子View排列下去,如此递归完成View树的排列
Draw
Draw过程相对简单,就是将View绘制到屏幕上方,分为以下几步:
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dispatchDraw),dispatchDraw方法会遍历调用所有子元素的draw方法,如此draw事件便一层一层传递下去了
- 绘制装饰(onDrawScrollBars)
自定义View
自定义View的作用和好处就不必多说了,绚丽的界面和花里胡哨的效果都离不开自定义View,一般3-5年Android从业人员手上都会积累大量的自定义View,帮助自己完成更好的开发
下面来看自定义View的几个分类
- 继承View重写onDraw()方法,这种做法需要自己支持wrap_content,否则会直接match_parent,padding也需要自己处理
- 继承ViewGroup派生特殊的Layout
- 继承特定的View(TextView),这个可以为自己的View添加生命周期,Lifecycle见得比较多
- 继承特定的ViewGroup(如LineayLayout)
自定义View比较容易出现各式各样的问题,常见的注意事项如下,欢迎补充:
- 因生命周期导致的内存泄漏问题
- 直接继承自view没有支持wrap_content和padding导致的显示问题
- 尽量不要在View中使用Handler
- View中有线程有动画没有即时停止
- View中有滑动冲突没有解决
直接继承自View
这种做法主要是为了显示一些不规则的效果,如绘制一个圆角控件
相关开源项目
圆角控件
六、RemoteView
RemoteView顾名思义就是远程View,是在其他应用或在进程上显示的View,就像各种APP的通知栏和QQ音乐的桌面切歌一样(当然肯定还有别的技术替代方案)
RemoteView的用法和案例上手相对较快,主要应用于通知栏和桌面小程序,这里主要复习一下RemoteView的机制
- RemoteView部件由NotificationManager和AppWidgetManager进行管理
- NotificationManager和AppWidgetManager通过Binder和SystemServer进程中对应的Service进行通信,对应的Service层负责完成布局文件,因而构成了跨进程通信
- RemoteView通过实现Parcelable接口进行跨进程通信,系统通过RemoteViews中的包名信息拿到对应资源,然后通过LayoutInflater加载布局文件
- 调用我们给RemoteView设置的一系列set方法更新View,这里NotificationManager和AppWidgetManager是通过添加Action对象,并提交给远程进程,最终远程进程收集完毕Action对象,遍历Action对象并调用apply方法,这里相当于是批量执行RemoteViews的修改操作从而避免大量的IPC操作
总结
View的底层设计还是很深厚的,值得花时间去学习去理解其中的设计思想
参考文章
《Android开发艺术探索》
《Android进阶之光》
内部拦截法与外部拦截法
自定义View使用与动画总结