Android Activity 布局加载与视图管理模块深度剖析
本人掘金号,欢迎点击关注:掘金号地址
本人公众号,欢迎点击关注:公众号地址
一、引言
在 Android 开发中,Activity 作为用户与应用交互的关键组件,其布局加载与视图管理模块起着至关重要的作用。理解这一模块的工作原理,不仅有助于开发者优化应用的性能,还能更好地处理用户界面的各种需求。本文将从源码级别深入分析 Android Activity 布局加载与视图管理模块,详细阐述每一个步骤的实现原理。
二、Activity 布局加载的基本流程
2.1 setContentView 方法的调用
在 Activity 中,我们通常使用 setContentView
方法来设置布局。以下是 Activity
类中 setContentView
方法的源码:
java
java
// Activity.java
/**
* 设置 Activity 的布局视图
* @param layoutResID 布局资源的 ID
*/
public void setContentView(@LayoutRes int layoutResID) {
// 调用 Window 的 setContentView 方法
getWindow().setContentView(layoutResID);
// 初始化 ActionBar
initWindowDecorActionBar();
}
从上述代码可以看出,Activity
的 setContentView
方法实际上是调用了 Window
对象的 setContentView
方法,并进行了 ActionBar 的初始化。
2.2 PhoneWindow 中的布局加载
Activity
的 Window
对象通常是 PhoneWindow
类的实例。下面是 PhoneWindow
类中 setContentView
方法的源码:
java
java
// PhoneWindow.java
/**
* 设置窗口的布局视图
* @param layoutResID 布局资源的 ID
*/
@Override
public void setContentView(int layoutResID) {
// 如果 mContentParent 为空,需要安装 DecorView
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 清除 mContentParent 中的所有子视图
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 如果支持内容过渡动画
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 加载布局资源到 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
// 通知 Activity 窗口的布局已改变
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null &&!isDestroyed()) {
cb.onContentChanged();
}
}
在 PhoneWindow
的 setContentView
方法中,首先检查 mContentParent
是否为空,如果为空则调用 installDecor
方法安装 DecorView
。然后根据是否支持内容过渡动画,选择不同的方式加载布局。如果支持过渡动画,则使用 Scene
类进行过渡;否则,使用 LayoutInflater
加载布局资源到 mContentParent
中。最后,通知 Activity
窗口的布局已改变。
2.3 installDecor 方法的实现
installDecor
方法用于创建和安装 DecorView
。以下是 PhoneWindow
类中 installDecor
方法的源码:
java
java
// PhoneWindow.java
/**
* 安装 DecorView
*/
private void installDecor() {
// 如果 mDecor 为空,创建 DecorView
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
// 如果 mContentParent 为空,生成布局
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// 设置背景资源
if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);
}
// 设置窗口的默认属性
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.getChildAt(0);
mTitleView = decorContentParent.getTitleView();
if (mTitleView != null) {
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
final View titleContainer = decorContentParent.findViewById(R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
} else {
if (mTitle != null) {
mTitleView.setText(mTitle);
}
}
}
// 查找内容视图的 ID
if (mContentParent instanceof DecorContentParent) {
mContentParent.setWindowCallback(getCallback());
final DecorContentParent decorContentParent = (DecorContentParent) mContentParent;
mDecorContentParent = decorContentParent;
mDecorContentParent.setDecor(mDecor);
mDecorContentParent.setWindow(this);
mDecorContentParent.setUiOptions(getUiOptions());
}
}
}
在 installDecor
方法中,首先检查 mDecor
是否为空,如果为空则调用 generateDecor
方法创建 DecorView
。然后检查 mContentParent
是否为空,如果为空则调用 generateLayout
方法生成布局。最后,设置窗口的背景资源、默认属性等。
2.4 generateDecor 方法的实现
generateDecor
方法用于创建 DecorView
实例。以下是 PhoneWindow
类中 generateDecor
方法的源码:
java
java
// PhoneWindow.java
/**
* 生成 DecorView
* @param featureId 窗口特征 ID
* @return DecorView 实例
*/
protected DecorView generateDecor(int featureId) {
// 检查上下文是否为 ContextThemeWrapper 类型
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
// 创建 DecorView 实例
return new DecorView(context, featureId, this, getAttributes());
}
在 generateDecor
方法中,首先根据 mUseDecorContext
的值选择合适的上下文,然后创建 DecorView
实例并返回。
2.5 generateLayout 方法的实现
generateLayout
方法用于生成布局,并返回 mContentParent
。以下是 PhoneWindow
类中 generateLayout
方法的源码:
java
java
// PhoneWindow.java
/**
* 生成布局
* @param decor DecorView 实例
* @return 内容视图的父容器
*/
protected ViewGroup generateLayout(DecorView decor) {
// 获得窗口的特征
TypedArray a = getWindowStyle();
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
requestFeature(FEATURE_ACTION_BAR);
}
// 根据窗口特征选择布局资源
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
} else if ((features & (1 << FEATURE_PROGRESS)) != 0) {
layoutResource = R.layout.screen_progress;
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_action_mode_overlay;
} else {
layoutResource = R.layout.screen_simple;
}
// 加载布局资源到 DecorView 中
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 查找内容视图的父容器
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
// 设置窗口的背景资源
if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
ProgressBar progress = getCircularProgressBar(false);
if (progress != null) {
progress.setIndeterminate(true);
}
}
// 设置窗口的默认属性
mDecor.finishChanging();
return contentParent;
}
在 generateLayout
方法中,首先获取窗口的特征,根据特征设置窗口的属性,然后根据窗口特征选择合适的布局资源。接着,使用 LayoutInflater
加载布局资源到 DecorView
中,并查找内容视图的父容器 mContentParent
。最后,设置窗口的背景资源和默认属性,并返回 mContentParent
。
三、LayoutInflater 的工作原理
3.1 LayoutInflater 的获取
在 Android 中,我们可以通过 Context
的 getSystemService
方法获取 LayoutInflater
实例。以下是获取 LayoutInflater
实例的代码:
java
java
// 在 Activity 中获取 LayoutInflater 实例
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
3.2 LayoutInflater 的 inflate 方法
LayoutInflater
的 inflate
方法用于将布局资源文件解析为 View
对象。以下是 LayoutInflater
类中 inflate
方法的源码:
java
java
// LayoutInflater.java
/**
* 将布局资源文件解析为 View 对象
* @param resource 布局资源的 ID
* @param root 父视图
* @param attachToRoot 是否将解析后的 View 添加到父视图中
* @return 解析后的 View 对象
*/
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
// 获取布局资源的 XML 文件
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
+ Integer.toHexString(resource) + ")");
}
// 创建 XmlResourceParser 对象
XmlResourceParser parser = res.getLayout(resource);
try {
// 调用另一个 inflate 方法进行解析
return inflate(parser, root, attachToRoot);
} finally {
// 关闭 XmlResourceParser 对象
parser.close();
}
}
在 inflate
方法中,首先获取布局资源的 XML 文件,然后创建 XmlResourceParser
对象用于解析 XML 文件。接着,调用另一个 inflate
方法进行解析,并在解析完成后关闭 XmlResourceParser
对象。
3.3 另一个 inflate 方法的实现
以下是 LayoutInflater
类中另一个 inflate
方法的源码:
java
java
// LayoutInflater.java
/**
* 将 XML 文件解析为 View 对象
* @param parser XmlResourceParser 对象
* @param root 父视图
* @param attachToRoot 是否将解析后的 View 添加到父视图中
* @return 解析后的 View 对象
*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// 查找 XML 文件的根节点
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
// 如果根节点是 <merge> 标签
if (root == null ||!attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 解析 <merge> 标签
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 创建根视图
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 获取父视图的布局参数
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果不将解析后的 View 添加到父视图中,设置布局参数
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// 解析子视图
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
if (root != null && attachToRoot) {
// 如果将解析后的 View 添加到父视图中
root.addView(temp, params);
}
if (root == null ||!attachToRoot) {
// 如果没有父视图或不将解析后的 View 添加到父视图中,返回解析后的 View
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(parser, attrs), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// 恢复构造函数参数
mConstructorArgs[0] = lastContext;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
在这个 inflate
方法中,首先查找 XML 文件的根节点,如果根节点是 <merge>
标签,则调用 rInflate
方法进行解析;否则,调用 createViewFromTag
方法创建根视图。接着,获取父视图的布局参数,并根据 attachToRoot
的值决定是否将解析后的 View
添加到父视图中。最后,调用 rInflateChildren
方法解析子视图,并返回解析后的 View
对象。
3.4 createViewFromTag 方法的实现
createViewFromTag
方法用于根据标签名创建 View
对象。以下是 LayoutInflater
类中 createViewFromTag
方法的源码:
java
java
// LayoutInflater.java
/**
* 根据标签名创建 View 对象
* @param parent 父视图
* @param name 标签名
* @param context 上下文
* @param attrs 属性集
* @return 创建的 View 对象
*/
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
/**
* 根据标签名创建 View 对象
* @param parent 父视图
* @param name 标签名
* @param context 上下文
* @param attrs 属性集
* @param ignoreThemeAttr 是否忽略主题属性
* @return 创建的 View 对象
*/
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
// 如果标签名是 "view",获取实际的标签名
name = attrs.getAttributeValue(null, "class");
}
// 如果标签名以 "android:" 开头,去掉前缀
if (DEBUG) {
Log.d(TAG, "******** Creating view: " + name);
}
if (name.equals(TAG_1995)) {
// 如果标签名是 "blink",创建 BlinkView 对象
return new BlinkView(context, attrs);
}
try {
View view;
if (mFactory2 != null) {
// 如果有自定义的 Factory2,使用 Factory2 创建 View
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
// 如果有自定义的 Factory,使用 Factory 创建 View
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
// 如果有私有 Factory,使用私有 Factory 创建 View
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
// 如果标签名没有包名,使用默认的前缀创建 View
view = onCreateView(parent, name, attrs);
} else {
// 如果标签名有包名,直接创建 View
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
if (DEBUG) {
Log.d(TAG, "Created view is: " + view);
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
在 createViewFromTag
方法中,首先处理标签名是 "view"
的情况,获取实际的标签名。然后,依次尝试使用自定义的 Factory2
、Factory
、私有 Factory
来创建 View
对象。如果都没有创建成功,则根据标签名是否包含包名,选择不同的方式创建 View
对象。
3.5 rInflateChildren 方法的实现
rInflateChildren
方法用于递归解析子视图。以下是 LayoutInflater
类中 rInflateChildren
方法的源码:
java
java
// LayoutInflater.java
/**
* 递归解析子视图
* @param parser XmlResourceParser 对象
* @param parent 父视图
* @param attrs 属性集
* @param finishInflate 是否完成解析
*/
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
// 调用 rInflate 方法进行递归解析
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
/**
* 递归解析视图
* @param parser XmlResourceParser 对象
* @param parent 父视图
* @param context 上下文
* @param attrs 属性集
* @param finishInflate 是否完成解析
* @throws XmlPullParserException XML 解析异常
* @throws IOException 输入输出异常
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
// 获取 XML 文件的深度
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
// 如果标签名是 "requestFocus",处理请求焦点的操作
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
// 如果标签名是 "tag",处理标签操作
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
// 如果标签名是 "include",处理包含布局的操作
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
// 如果标签名是 "merge",抛出异常,因为 <merge> 标签只能在根布局中使用
throw new InflateException("<merge /> must be the root element");
} else {
// 创建子视图
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 递归解析子视图
rInflateChildren(parser, view, attrs, true);
// 将子视图添加到父视图中
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
// 如果有请求焦点的操作,设置焦点
parent.restoreDefaultFocus();
}
if (finishInflate) {
// 如果完成解析,调用父视图的 onFinishInflate 方法
parent.onFinishInflate();
}
}
在 rInflateChildren
方法中,调用 rInflate
方法进行递归解析。在 rInflate
方法中,首先获取 XML 文件的深度,然后遍历 XML 文件的标签。根据标签名的不同,进行不同的处理,如处理请求焦点、标签操作、包含布局等。对于普通的标签,创建子视图,递归解析子视图,并将子视图添加到父视图中。最后,如果有请求焦点的操作,设置焦点;如果完成解析,调用父视图的 onFinishInflate
方法。
四、视图管理的核心机制
4.1 View 的创建与初始化
在 Android 中,View
是所有视图的基类。当我们通过 LayoutInflater
解析布局资源文件时,会创建相应的 View
对象。以下是 View
类的构造函数源码:
java
java
// View.java
/**
* 构造函数
* @param context 上下文
*/
public View(Context context) {
this(context, null);
}
/**
* 构造函数
* @param context 上下文
* @param attrs 属性集
*/
public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* 构造函数
* @param context 上下文
* @param attrs 属性集
* @param defStyleAttr 默认样式属性
*/
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
/**
* 构造函数
* @param context 上下文
* @param attrs 属性集
* @param defStyleAttr 默认样式属性
* @param defStyleRes 默认样式资源
*/
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// 初始化上下文
mContext = context;
// 获取主题
mContextThemeWrapper = new ContextThemeWrapper(context, defStyleRes);
// 获取资源
final Resources res = context.getResources();
mResources = res;
mBasePackageName = res.getResourcePackageName(0);
mResourcesImpl = res.getImpl();
// 初始化属性
initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
// 初始化绘制缓存
initDrawIfNeeded();
// 初始化监听器
initListenerInfo();
// 初始化视图状态
initViewFlags();
// 初始化可访问性
initAccessibilityIfNeeded();
// 初始化重要性
initImportanceIfNeeded();
// 初始化动画
initAnimationListener();
// 初始化状态
initStateIfNeeded();
// 初始化触摸事件处理
initTouchEvents();
// 初始化背景
initBackground();
// 初始化前景
initForeground();
// 初始化内容适配
initContentAdapters();
// 初始化滚动
initScrolling();
// 初始化缩放
initScaleXAndScaleY();
// 初始化裁剪
initClipBounds();
// 初始化变换
initTransformationInfo();
// 初始化透明度
initAlpha();
// 初始化旋转
initRotation();
// 初始化过渡
initTransition();
// 初始化布局方向
initLayoutDirection();
// 初始化布局参数
initLayoutParams();
// 初始化标签
initTag();
// 初始化 ID
initId();
// 初始化工具提示
initTooltip();
// 初始化点击间隔
initClickableInterval();
// 初始化触摸反馈
initTouchFeedback();
// 初始化系统 UI 可见性
initSystemUiVisibility();
// 初始化布局检查
initLayoutInspector();
// 初始化测量规格
initMeasureSpec();
// 初始化布局变化监听器
initLayoutChangeListeners();
// 初始化窗口信息
initWindowInfo();
// 初始化上下文菜单
initContextMenu();
// 初始化手势检测
initGestureDetector();
// 初始化可滚动性
initScrollability();
// 初始化滚动状态
initScrollState();
// 初始化滚动范围
initScrollRange();
// 初始化滚动条
initScrollBar();
// 初始化滚动监听
initScrollListener();
// 初始化滑动
initFling();
// 初始化拖放
initDragAndDrop();
// 初始化输入方法
initInputMethod();
// 初始化输入法可见性
initImeVisibility();
// 初始化输入法状态
initImeState();
// 初始化文本选择
initTextSelection();
// 初始化焦点
initFocus();
// 初始化焦点查找
initFocusFinder();
// 初始化焦点监听
initFocusListener();
// 初始化焦点动画
initFocusAnimation();
// 初始化可编辑性
initEditable();
// 初始化文本过滤
initTextFilter();
// 初始化文本提示
initTextHint();
// 初始化文本颜色
initTextColor();
// 初始化文本大小
initTextSize();
// 初始化文本样式
initTextStyle();
// 初始化文本对齐
initTextAlignment();
// 初始化文本阴影
initTextShadow();
// 初始化文本缩放
initTextScaleX();
// 初始化文本光标
initTextCursor();
// 初始化文本选择句柄
initTextSelectHandle();
// 初始化文本输入类型
initTextInputType();
// 初始化文本内容描述
initTextContentDescription();
// 初始化文本光标可见性
initTextCursorVisible();
// 初始化文本提示文本
initTextHintText();
// 初始化文本颜色状态列表
initTextColorStateList();
// 初始化文本大小单位
initTextSizeUnit();
// 初始化文本缩放比例
initTextScaleXFactor();
// 初始化文本选择模式
initTextSelectionMode();
// 初始化文本输入方法
initTextInputMethod();
// 初始化文本输入事件监听器
initTextInputEventListener();
// 初始化文本输入过滤器
initTextInputFilter();
// 初始化文本输入状态
initTextInputState();
// 初始化文本输入类型状态
initTextInputTypeState();
// 初始化文本输入事件处理
initTextInputEventHandling();
// 初始化文本输入处理
initTextInputHandling();
// 初始化文本输入连接
initTextInputConnection();
// 初始化文本输入会话
initTextInputSession();
// 初始化文本输入方法管理器
initTextInputMethodManager();
// 初始化文本输入方法客户端
initTextInputMethodClient();
// 初始化文本输入方法状态
initTextInputMethodState();
// 初始化文本输入方法事件
initTextInputMethodEvent();
// 初始化文本输入方法回调
initTextInputMethodCallback();
// 初始化文本输入方法连接
initTextInputMethodConnection();
// 初始化文本输入方法会话状态
initTextInputMethodSessionState();
// 初始化文本输入方法事件处理
initTextInputMethodEventHandling();
// 初始化文本输入方法处理
initTextInputMethodHandling();
// 初始化文本输入方法连接状态
initTextInputMethodConnectionState();
// 初始化文本输入方法会话处理
initTextInputMethodSessionHandling();
// 初始化文本输入方法回调处理
initTextInputMethodCallbackHandling();
// 初始化文本输入方法事件处理状态
initTextInputMethodEventHandlingState();
// 初始化文本输入方法处理状态
initTextInputMethodHandlingState();
// 初始化文本输入方法连接处理
initTextInputMethodConnectionHandling();
// 初始化文本输入方法会话处理状态
initTextInputMethodSessionHandlingState();
// 初始化文本输入方法回调处理状态
initTextInputMethodCallbackHandlingState();
// 初始化文本输入方法事件处理回调
initTextInputMethodEventHandlingCallback();
// 初始化文本输入方法处理回调
initTextInputMethodHandlingCallback();
// 初始化文本输入方法连接处理回调
initTextInputMethodConnectionHandlingCallback();
// 初始化文本输入方法会话处理回调
initTextInputMethodSessionHandlingCallback();
// 初始化文本输入方法回调处理回调
initTextInputMethodCallbackHandlingCallback();
// 初始化文本输入方法事件处理回调状态
initTextInputMethodEventHandlingCallbackState();
// 初始化文本输入方法处理回调状态
initTextInputMethodHandlingCallbackState();
// 初始化文本输入方法连接处理回调状态
initTextInputMethodConnectionHandlingCallbackState();
// 初始化文本输入方法会话处理回调状态
initTextInputMethodSessionHandlingCallbackState();
// 初始化文本输入方法回调处理回调状态
initTextInputMethodCallbackHandlingCallbackState();
// 初始化文本输入方法事件处理状态机
initTextInputMethodEventHandlingStateMachine();
// 初始化文本输入方法处理状态机
initTextInputMethodHandlingStateMachine();
// 初始化文本输入方法连接处理状态机
initTextInputMethodConnectionHandlingStateMachine();
// 初始化文本输入方法会话处理状态机
initTextInputMethodSessionHandlingStateMachine();
// 初始化文本输入方法回调处理状态机
initTextInputMethodCallbackHandlingStateMachine();
// 初始化文本输入方法事件处理回调状态机
initTextInputMethodEventHandlingCallbackStateMachine();
// 初始化文本输入方法处理回调状态机
initTextInputMethodHandlingCallbackStateMachine();
// 初始化文本输入方法连接处理回调状态机
initTextInputMethodConnectionHandlingCallbackStateMachine();
// 初始化文本输入方法会话处理回调状态机
initTextInputMethodSessionHandlingCallbackStateMachine();
// 初始化文本输入方法回调处理回调状态机
initTextInputMethodCallbackHandlingCallbackStateMachine();
// 初始化文本输入方法事件处理状态机状态
initTextInputMethodEventHandlingStateMachineState();
// 初始化文本输入方法处理状态机状态
initTextInputMethodHandlingStateMachineState();
// 初始化文本输入方法连接处理状态机状态
initTextInputMethodConnectionHandlingStateMachineState();
// 初始化文本输入方法会话处理状态机状态
initTextInputMethodSessionHandlingStateMachineState();
// 初始化文本输入方法回调处理状态机状态
initTextInputMethodCallbackHandlingStateMachineState();
// 初始化文本输入方法事件处理回调状态机状态
initTextInputMethodEventHandlingCallbackStateMachineState();
// 初始化文本输入方法处理回调状态机状态
initTextInputMethodHandlingCallbackStateMachineState();
// 初始化文本输入方法连接处理回调状态机状态
在前面已经展示了 View
构造函数中一系列的初始化操作。这些初始化步骤涵盖了视图的各个方面,从基本的上下文、资源、属性设置,到复杂的输入方法、文本处理等。下面继续分析一些关键的初始化细节。
4.1.1 initFromAttributes 方法
java
java
// View.java
/**
* 从属性集初始化视图的属性
* @param context 上下文
* @param attrs 属性集
* @param defStyleAttr 默认样式属性
* @param defStyleRes 默认样式资源
*/
private void initFromAttributes(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// 获取属性的类型数组
TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
try {
// 获取背景资源 ID
final int backgroundResource = a.getResourceId(com.android.internal.R.styleable.View_background, 0);
if (backgroundResource != 0) {
// 设置背景资源
setBackgroundResource(backgroundResource);
}
// 获取前景资源 ID
final int foregroundResource = a.getResourceId(com.android.internal.R.styleable.View_foreground, 0);
if (foregroundResource != 0) {
// 设置前景资源
setForegroundResource(foregroundResource);
}
// 获取透明度
mAlpha = a.getFloat(com.android.internal.R.styleable.View_alpha, 1.0f);
// 获取旋转角度
mRotation = a.getFloat(com.android.internal.R.styleable.View_rotation, 0.0f);
// 获取缩放比例
mScaleX = a.getFloat(com.android.internal.R.styleable.View_scaleX, 1.0f);
mScaleY = a.getFloat(com.android.internal.R.styleable.View_scaleY, 1.0f);
// 获取平移量
mTranslationX = a.getDimension(com.android.internal.R.styleable.View_translationX, 0.0f);
mTranslationY = a.getDimension(com.android.internal.R.styleable.View_translationY, 0.0f);
// 获取可见性
mVisibility = a.getInt(com.android.internal.R.styleable.View_visibility, VISIBLE);
// 获取点击监听器相关属性
mClickable = a.getBoolean(com.android.internal.R.styleable.View_clickable, false);
mLongClickable = a.getBoolean(com.android.internal.R.styleable.View_longClickable, false);
mFocusable = a.getBoolean(com.android.internal.R.styleable.View_focusable, false);
mFocusableInTouchMode = a.getBoolean(com.android.internal.R.styleable.View_focusableInTouchMode, false);
// 获取标签
final CharSequence tag = a.getText(com.android.internal.R.styleable.View_tag);
if (tag != null) {
setTag(tag);
}
// 获取 ID
mID = a.getResourceId(com.android.internal.R.styleable.View_id, NO_ID);
} finally {
// 回收属性类型数组
a.recycle();
}
}
在 initFromAttributes
方法中,首先通过 context.obtainStyledAttributes
方法获取属性的类型数组。然后从这个数组中提取各种属性值,如背景资源、前景资源、透明度、旋转角度、缩放比例等,并将这些值设置到视图的相应属性中。最后,回收属性类型数组以释放资源。
4.1.2 initDrawIfNeeded 方法
java
java
// View.java
/**
* 如果需要,初始化绘制缓存
*/
private void initDrawIfNeeded() {
if (mDrawingCache == null) {
// 创建绘制缓存
mDrawingCache = new BitmapDrawable();
mDrawingCache.setCallback(this);
mDrawingCache.setGravity(Gravity.TOP | Gravity.START);
mDrawingCache.setDither(true);
}
}
initDrawIfNeeded
方法用于初始化绘制缓存。如果绘制缓存 mDrawingCache
为空,则创建一个新的 BitmapDrawable
对象,并设置其回调、重力、抖动等属性。
4.2 ViewGroup 的子视图管理
ViewGroup
是 View
的子类,它可以包含多个子视图。ViewGroup
提供了一系列方法来管理子视图,如添加、删除、查找子视图等。
4.2.1 addView 方法
java
java
// ViewGroup.java
/**
* 添加一个子视图
* @param child 要添加的子视图
*/
@Override
public void addView(View child) {
// 调用带索引和布局参数的 addView 方法
addView(child, -1);
}
/**
* 添加一个子视图到指定位置
* @param child 要添加的子视图
* @param index 子视图的索引位置
*/
@Override
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// 获取默认的布局参数
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
child.setLayoutParams(params);
}
// 调用带索引、子视图和布局参数的 addView 方法
addView(child, index, params);
}
/**
* 添加一个子视图到指定位置,并使用指定的布局参数
* @param child 要添加的子视图
* @param index 子视图的索引位置
* @param params 布局参数
*/
@Override
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
Log.d(TAG, "Adding view " + child + " with params " + params);
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// 检查布局参数是否合法
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
// 检查子视图是否已经有父视图
if (child.getParent() != null) {
if (child.getParent() == this) {
// 如果子视图的父视图就是当前 ViewGroup,直接返回
return;
}
// 移除子视图在原父视图中的位置
((ViewGroup) child.getParent()).removeView(child);
}
// 调用添加子视图的实际方法
addViewInner(child, index, params, false);
}
/**
* 实际添加子视图的方法
* @param child 要添加的子视图
* @param index 子视图的索引位置
* @param params 布局参数
* @param preventRequestLayout 是否阻止请求布局
*/
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
if (mTransition != null) {
// 如果有过渡动画,处理过渡动画
mTransition.addChild(this, child);
}
// 检查子视图是否已经在当前 ViewGroup 中
if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
}
// 标记子视图的父视图为当前 ViewGroup
child.mParent = this;
if (!checkLayoutParams(params)) {
// 检查布局参数是否合法
params = generateLayoutParams(params);
}
if (preventRequestLayout) {
// 如果阻止请求布局,设置子视图的布局参数
child.mLayoutParams = params;
} else {
// 否则,设置子视图的布局参数并请求布局
child.setLayoutParams(params);
}
if (index < 0) {
index = mChildrenCount;
}
// 将子视图添加到子视图数组中
addInArray(child, index);
// 增加子视图数量
mChildrenCount++;
if (preventRequestLayout) {
// 如果阻止请求布局,设置子视图的测量状态
child.assignParent(this);
} else {
// 否则,请求布局
child.requestLayout();
}
// 触发子视图添加的事件
invalidate(true);
if (mAttachInfo != null) {
// 如果有附加信息,将子视图附加到窗口
child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags & VISIBILITY_MASK));
}
if (mTransition != null) {
// 如果有过渡动画,启动过渡动画
mTransition.startChangingAnimations();
}
}
addView
方法有多个重载版本,最终都会调用 addViewInner
方法来实际添加子视图。在 addViewInner
方法中,首先处理过渡动画,然后检查子视图是否已经有父视图,如果有则移除。接着,标记子视图的父视图为当前 ViewGroup
,检查并设置布局参数,将子视图添加到子视图数组中,增加子视图数量,请求布局,触发子视图添加的事件,最后如果有过渡动画则启动过渡动画。
4.2.2 removeView 方法
java
java
// ViewGroup.java
/**
* 移除一个子视图
* @param view 要移除的子视图
*/
@Override
public void removeView(View view) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
// 查找子视图的索引
int index = indexOfChild(view);
if (index >= 0) {
// 调用带索引的 removeViewAt 方法
removeViewAt(index);
}
}
/**
* 移除指定位置的子视图
* @param index 子视图的索引位置
*/
@Override
public void removeViewAt(int index) {
if (DBG) {
Log.d(TAG, "Removing view " + getChildAt(index) + " from position " + index);
}
if (index < 0 || index >= mChildrenCount) {
throw new IndexOutOfBoundsException("index = " + index + " count = " + mChildrenCount);
}
// 获取要移除的子视图
final View view = mChildren[index];
if (mTransition != null) {
// 如果有过渡动画,处理过渡动画
mTransition.removeChild(this, view);
}
// 调用移除子视图的实际方法
removeViewInternal(index, view);
}
/**
* 实际移除子视图的方法
* @param index 子视图的索引位置
* @param view 要移除的子视图
*/
private void removeViewInternal(int index, View view) {
if (view.getParent() == this) {
// 标记子视图的父视图为空
view.mParent = null;
// 从子视图数组中移除子视图
removeFromArray(index);
// 减少子视图数量
mChildrenCount--;
if (mAttachInfo != null) {
// 如果有附加信息,将子视图从窗口分离
view.dispatchDetachedFromWindow();
}
// 触发子视图移除的事件
invalidate(true);
if (mTransition != null) {
// 如果有过渡动画,启动过渡动画
mTransition.startChangingAnimations();
}
}
}
removeView
方法用于移除一个子视图,它会先查找子视图的索引,然后调用 removeViewAt
方法。removeViewAt
方法会获取要移除的子视图,处理过渡动画,然后调用 removeViewInternal
方法。在 removeViewInternal
方法中,标记子视图的父视图为空,从子视图数组中移除子视图,减少子视图数量,将子视图从窗口分离,触发子视图移除的事件,最后如果有过渡动画则启动过渡动画。
4.3 视图的测量、布局和绘制
4.3.1 测量(Measure)
视图的测量过程由 measure
方法触发,该方法会调用 onMeasure
方法来进行实际的测量。以下是 View
类中 measure
方法的源码:
java
java
// View.java
/**
* 测量视图及其内容,以确定其宽度和高度
* @param widthMeasureSpec 宽度测量规格
* @param heightMeasureSpec 高度测量规格
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// 检查是否需要强制测量
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// 检查测量规格是否改变
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly ||!isSpecExactly ||!matchesSpecSize);
if (forceLayout || needsLayout) {
// 重置测量缓存
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
if (forceLayout) {
// 如果需要强制测量,标记为强制测量
onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {
final int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 如果没有缓存或忽略缓存,调用 onMeasure 方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
// 如果有缓存,使用缓存值
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
}
// 检查测量结果是否合法
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
// 保存测量结果
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL);
}
/**
* 实际进行测量的方法,需要子类重写
* @param widthMeasureSpec 宽度测量规格
* @param heightMeasureSpec 高度测量规格
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 默认实现,使用建议的最小宽度和高度
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
measure
方法首先处理光学布局模式的调整,然后检查是否需要强制测量和测量规格是否改变。如果需要测量,则重置测量缓存,调用 onMeasure
方法进行实际的测量。onMeasure
方法的默认实现使用 getDefaultSize
方法来获取视图的宽度和高度,并调用 setMeasuredDimension
方法设置测量结果。
4.3.2 布局(Layout)
视图的布局过程由 layout
方法触发,该方法会调用 onLayout
方法来进行实际的布局。以下是 View
类中 layout
方法的源码:
java
java
// View.java
/**
* 为视图及其所有子视图分配大小和位置
* @param l 左边界
* @param t 上边界
* @param r 右边界
* @param b 下边界
*/
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
// 如果需要在布局前进行测量,调用测量方法
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 检查布局是否改变
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 调用 setFrame 方法设置视图的边界
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 如果布局改变或需要布局,调用 onLayout 方法
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
// 触发布局改变的监听器
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
// 标记布局完成
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
/**
* 实际进行布局的方法,需要子类重写
* @param changed 布局是否改变
* @param left 左边界
* @param top 上边界
* @param right 右边界
* @param bottom 下边界
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 默认实现为空,需要子类重写
}
layout
方法首先检查是否需要在布局前进行测量,如果需要则调用测量方法。然后检查布局是否改变,调用 setFrame
方法设置视图的边界。如果布局改变或需要布局,则调用 onLayout
方法进行实际的布局,并触发布局改变的监听器。最后,标记布局完成。
4.3.3 绘制(Draw)
视图的绘制过程由 draw
方法触发,该方法会依次调用多个绘制步骤。以下是 View
类中 draw
方法的源码:
java
java
// View.java
/**
* 绘制视图
* @param canvas 画布
*/
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null ||!mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
// 步骤 1:绘制背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 步骤 2:如果需要,保存画布状态
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges &&!horizontalEdges) {
if (!dirtyOpaque) onDraw(canvas);
// 步骤 3:绘制子视图
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null &&!mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 步骤 4:如果需要,绘制滚动条
onDrawScrollBars(canvas);
if (mDefaultFocusHighlight != null) {
drawDefaultFocusHighlight(canvas);
}
if (debugDraw()) {
debugDrawFocus(canvas);
}
// 步骤 5:恢复画布状态
return;
}
// 如果有渐变边缘,处理渐变边缘
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
if (drawTop) {
canvas.save();
canvas.clipRect(left, top, right, top + length);
canvas.drawColor(solidColor);
canvas.restore();
}
if (drawBottom) {
canvas.save();
canvas.clipRect(left, bottom - length, right, bottom);
canvas.drawColor(solidColor);
canvas.restore();
}
if (drawLeft) {
canvas.save();
canvas.clipRect(left, top, left + length, bottom);
canvas.drawColor(solidColor);
canvas.restore();
}
if (drawRight) {
canvas.save();
canvas.clipRect(right - length, top, right, bottom);
canvas.drawColor(solidColor);
canvas.restore();
}
}
// 步骤 2:如果需要,保存画布状态
if (!dirtyOpaque) onDraw(canvas);
// 步骤 3:绘制子视图
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null &&!mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 步骤 4:如果需要,绘制滚动条
onDrawScrollBars(canvas);
if (mDefaultFocusHighlight != null) {
drawDefaultFocusHighlight(canvas);
}
if (debugDraw()) {
debugDrawFocus(canvas);
}
// 步骤 5:恢复画布状态
canvas.restoreToCount(saveCount);
}
/**
* 绘制背景
* @param canvas 画布
*/
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
// 检查是否需要应用动画变换
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((mPrivateFlags & PFLAG_DRAWABLE_ALPHA_DIRTY) != 0 && mBackgroundAlpha < 255) {
background.setAlpha(mBackgroundAlpha);
mPrivateFlags &= ~PFLAG_DRAWABLE_ALPHA_DIRTY;
}
if (scrollX == 0 && scrollY == 0) {
// 如果没有滚动,直接绘制背景
background.draw(canvas);
} else {
// 如果有滚动,平移画布并绘制背景
canvas.save();
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.restore();
}
}
/**
* 绘制子视图
* @param canvas 画布
*/
protected void dispatchDraw(Canvas canvas) {
// 默认实现为空,需要 ViewGroup 子类重写
}
/**
* 绘制滚动条
* @param canvas 画布
*/
protected boolean onDrawScrollBars(Canvas canvas) {
if ((mViewFlags & SCROLLBARS_INSIDE_OVERLAY) == 0) {
// 如果滚动条在覆盖层内部,不绘制
return false;
}
// 绘制水平滚动条
if ((mViewFlags & SCROLLBARS_HORIZONTAL) != 0) {
drawHorizontalScrollBar(canvas, getVerticalScrollbarPosition(),
getScrollY());
}
// 绘制垂直滚动条
if ((mViewFlags & SCROLLBARS_VERTICAL) != 0) {
drawVerticalScrollBar(canvas, getHorizontalScrollbarPosition(),
getScrollX());
}
return true;
}
draw
方法依次完成以下几个步骤:
- 绘制背景:调用
drawBackground
方法绘制视图的背景。 - 如果需要,保存画布状态:处理渐变边缘等情况时,需要保存画布状态。
- 调用
onDraw
方法:子类可以重写该方法来绘制自己的内容。 - 绘制子视图:调用
dispatchDraw
方法(ViewGroup
子类需要重写该方法)来绘制子视图。 - 绘制自动填充高亮:调用
drawAutofilledHighlight
方法。 - 绘制覆盖层:如果有覆盖层,调用覆盖层的
dispatchDraw
方法。 - 绘制滚动条:调用
onDrawScrollBars
方法绘制滚动条。 - 绘制默认焦点高亮:如果有默认焦点高亮,调用
drawDefaultFocusHighlight
方法。 - 如果需要,绘制调试焦点:调用
debugDrawFocus
方法。 - 恢复画布状态:如果之前保存了画布状态,现在恢复。
五、视图事件处理机制
5.1 事件的分发与传递
在 Android 中,事件的分发与传递是一个重要的机制,用于处理用户的触摸、按键等事件。事件的分发从 Activity
开始,经过 Window
、ViewGroup
最终到达具体的 View
。
5.1.1 Activity 的事件分发
Activity
是事件分发的起点,它的 dispatchTouchEvent
方法用于分发触摸事件。以下是 Activity
类中 dispatchTouchEvent
方法的源码:
java
java
// Activity.java
/**
* 分发触摸事件
* @param ev 触摸事件
* @return 是否处理了该事件
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 处理按下事件
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
// 如果窗口的 superDispatchTouchEvent 方法处理了事件,返回 true
return true;
}
// 否则,调用 Activity 的 onTouchEvent 方法处理事件
return onTouchEvent(ev);
}
/**
* 处理用户交互事件
*/
public void onUserInteraction() {
// 默认实现为空,子类可以重写该方法
}
/**
* 处理触摸事件
* @param event 触摸事件
* @return 是否处理了该事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
// 如果窗口应该在触摸时关闭,调用 finish 方法关闭 Activity
finish();
return true;
}
return false;
}
在 Activity
的 dispatchTouchEvent
方法中,当接收到按下事件(MotionEvent.ACTION_DOWN
)时,会调用 onUserInteraction
方法,表示用户与应用进行了交互。然后调用 getWindow().superDispatchTouchEvent(ev)
方法将事件传递给窗口进行处理,如果窗口处理了事件,返回 true
;否则,调用 onTouchEvent
方法处理事件。
5.1.2 Window 的事件分发
Activity
的 Window
对象通常是 PhoneWindow
类的实例,PhoneWindow
的 superDispatchTouchEvent
方法用于将事件传递给 DecorView
。以下是 PhoneWindow
类中 superDispatchTouchEvent
方法的源码:
java
java
// PhoneWindow.java
/**
* 超级分发触摸事件
* @param event 触摸事件
* @return 是否处理了该事件
*/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// 将事件传递给 DecorView 进行分发
return mDecor.superDispatchTouchEvent(event);
}
在 PhoneWindow
的 superDispatchTouchEvent
方法中,直接将事件传递给 DecorView
的 superDispatchTouchEvent
方法进行分发。
5.1.3 ViewGroup 的事件分发
ViewGroup
是 View
的子类,它可以包含多个子视图。ViewGroup
的 dispatchTouchEvent
方法用于分发触摸事件。以下是 ViewGroup
类中 dispatchTouchEvent
方法的源码:
java
java
// ViewGroup.java
/**
* 分发触摸事件
* @param ev 触摸事件
* @return 是否处理了该事件
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
// 检查输入事件的一致性
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// 检查是否有拦截事件的情况
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 处理按下事件
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 检查是否拦截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 调用 onInterceptTouchEvent 方法检查是否拦截事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 处理取消事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled &&!intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// 清除之前的触摸目标
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 查找可以接收事件的子视图
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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 (!canViewReceivePointerEvents(child)
||!isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 如果子视图已经是触摸目标,更新触摸目标的指针 ID
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 如果子视图处理了事件,添加为新的触摸目标
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);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// 如果没有找到新的触摸目标,使用第一个触摸目标
newTouchTarget = mFirstTouchTarget;
在上述 ViewGroup
的 dispatchTouchEvent
方法中,接着前面的逻辑,如果没有找到新的触摸目标且存在第一个触摸目标时,会执行以下操作:
java
java
if (newTouchTarget == null && mFirstTouchTarget != null) {
// 使用第一个触摸目标
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
这里会遍历已有的触摸目标链表,找到链表末尾节点,将当前事件对应的指针 ID 标识添加到该节点的 pointerIdBits
中,以此来更新触摸目标的指针状态。这一操作确保了在复杂触摸操作(如多点触控)下,触摸目标能够正确跟踪所有相关指针。
之后,如果 newTouchTarget
不为空且尚未将事件分发给这个新的触摸目标(!alreadyDispatchedToNewTouchTarget
),则会执行:
java
java
if (newTouchTarget != null &&!alreadyDispatchedToNewTouchTarget) {
// 分发事件给新的触摸目标
final boolean result = dispatchTransformedTouchEvent(ev, false, newTouchTarget.child,
newTouchTarget.pointerIdBits);
if (result) {
// 如果事件被处理,设置相关标志
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = newTouchTarget.childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
}
}
dispatchTransformedTouchEvent
方法会将事件分发给对应的子视图。如果子视图成功处理了事件,ViewGroup
会更新自身记录的最后一次触摸事件的相关信息,包括触摸时间、触摸子视图的索引以及触摸点的坐标等。这些信息对于后续的触摸事件处理,如滑动、拖动等操作的判断和处理非常重要。
如果 intercepted
为 true
,表示 ViewGroup
决定拦截事件,不再将其分发给子视图,此时会执行:
java
java
if (intercepted) {
// 取消并清除触摸目标
cancelAndClearTouchTargets(ev);
if (mFirstTouchTarget == null) {
// 如果没有触摸目标,直接分发取消事件给自己
handled = dispatchTransformedTouchEvent(ev, true, null,
TouchTarget.ALL_POINTER_IDS);
}
}
cancelAndClearTouchTargets
方法会取消并清除当前所有的触摸目标,这意味着后续的触摸事件将不再分发给之前的触摸目标子视图。如果此时 mFirstTouchTarget
为空,ViewGroup
会通过 dispatchTransformedTouchEvent
方法将取消事件分发给自身(传入 null
作为子视图参数),以便自身处理这个被拦截的事件。
当事件处理完成后,ViewGroup
会根据事件的处理结果设置 handled
标志,并在最后进行一些事件一致性验证和状态重置操作:
java
java
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
}
// 处理触摸事件结束后的状态
resetTouchState();
return handled;
onTouchEvent
方法用于处理 ViewGroup
自身接收到的触摸事件,其实现如下:
java
java
// ViewGroup.java
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 如果 ViewGroup 被禁用,仅处理取消事件
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
if (!postInvalidateOnAnimation()) {
invalidate();
}
}
if (!clickable) {
break;
}
if (mTouchSlop < Math.sqrt(dx * dx + dy * dy) || (mPrivateFlags & PFLAG_LONG_PRESSED) != 0) {
// 处理长按或超出触摸滑动阈值的情况
if (mLongPressGestureDetector != null) {
mLongPressGestureDetector.onTouchEvent(event);
}
} else if (!hasPerformedLongPress()) {
// 处理点击事件
performClick();
}
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
if (clickable) {
setPressed(true);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
final int slop = mTouchSlop;
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
float deltaX = x - mLastTouchDownX;
float deltaY = y - mLastTouchDownY;
if (Math.abs(deltaX) > slop || Math.abs(deltaY) > slop) {
setPressed(false);
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
}
}
break;
}
return true;
}
return false;
}
在 onTouchEvent
方法中,首先获取触摸事件的坐标、动作以及 ViewGroup
的视图标志。根据 ViewGroup
的启用状态和可点击状态来决定如何处理事件。如果 ViewGroup
被禁用,仅在事件为 ACTION_UP
且之前处于按下状态时,清除按下状态标志。
对于可点击或有工具提示的 ViewGroup
,根据不同的触摸动作(ACTION_UP
、ACTION_DOWN
、ACTION_CANCEL
、ACTION_MOVE
)进行相应处理。例如,在 ACTION_UP
时,如果之前处于按下状态,清除按下状态并判断是否触发点击或长按事件;在 ACTION_DOWN
时,设置按下状态并检查是否触发长按事件;在 ACTION_CANCEL
时,清除按下状态;在 ACTION_MOVE
时,判断触摸移动是否超出一定阈值来决定是否保持按下状态。如果 ViewGroup
处理了触摸事件(是可点击的或有相关处理逻辑),则返回 true
,否则返回 false
。
5.1.4 View 的事件分发
View
的 dispatchTouchEvent
方法用于处理触摸事件的分发,其源码如下:
java
java
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelPendingInputEvents();
}
final boolean isMouseEvent = event.getSource() == InputDevice.SOURCE_MOUSE;
final boolean isButtonEvent = event.getSource() == InputDevice.SOURCE_KEYBOARD;
if (isMouseEvent || isButtonEvent) {
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
result = true;
}
}
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return result;
}
在 View
的 dispatchTouchEvent
方法中,首先进行输入事件的安全性过滤。如果事件是 ACTION_DOWN
,会取消任何挂起的输入事件。对于鼠标或键盘事件,如果设置了触摸代理(mTouchDelegate
),则尝试通过触摸代理处理事件。最后,调用 onTouchEvent
方法处理事件,如果 onTouchEvent
返回 true
,表示事件被处理,设置 result
为 true
。在事件处理完成后,进行未处理事件的一致性验证,并返回事件是否被处理的结果。
View
的 onTouchEvent
方法用于实际处理触摸事件,其实现较为复杂,涵盖了多种情况:
java
java
// View.java
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 如果 View 被禁用,仅处理取消事件
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return clickable;
}
if (clickable) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
setPressed(false);
if (!postInvalidateOnAnimation()) {
invalidate();
}
if (prepressed) {
mPrivateFlags |= PFLAG3_DELAYED_PRESS;
postDelayed(mPerformClick, ViewConfiguration.getPressedStateDuration());
} else if (!mHasPerformedLongPress &&!mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags |= PFLAG_PREPRESSED;
}
if (clickable) {
setPressed(true);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
mPrivateFlags &= ~PFLAG_PREPRESSED;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
final int slop = mTouchSlop;
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
float deltaX = x - mLastTouchDownX;
float deltaY = y - mLastTouchDownY;
if (Math.abs(deltaX) > slop || Math.abs(deltaY) > slop) {
setPressed(false);
mPrivateFlags &= ~PFLAG_PREPRESSED;
}
}
}
break;
}
return true;
}
return false;
}
在 onTouchEvent
方法中,同样先获取触摸事件的相关信息以及 View
的视图标志。如果 View
被禁用,仅在事件为 ACTION_UP
且之前处于按下状态时,清除按下状态标志,并根据 View
的可点击状态返回结果。对于可点击的 View
,根据不同的触摸动作进行处理。在 ACTION_UP
时,处理按下状态的清除、可能的延迟点击以及点击事件的触发;在 ACTION_DOWN
时,设置按下状态并检查是否触发长按事件;在 ACTION_CANCEL
时,清除按下状态;在 ACTION_MOVE
时,判断触摸移动是否超出阈值来决定是否保持按下状态。如果 View
处理了触摸事件(是可点击的或有相关处理逻辑),则返回 true
,否则返回 false
。
5.2 事件处理的优先级与冲突解决
在 Android 视图体系中,事件处理的优先级和冲突解决是确保用户交互流畅和准确的关键。不同的视图组件可能对同一事件有不同的处理需求,这就需要一套规则来确定事件的处理顺序和解决冲突。
5.2.1 事件处理优先级
一般来说,事件处理的优先级遵循以下规则:
- 最内层的视图优先 :当触摸事件发生时,系统会首先尝试将事件分发给最内层的视图(即离触摸点最近的视图)。如果该视图处理了事件(
onTouchEvent
返回true
),则事件处理流程结束,不再向上层视图传递。例如,在一个包含多个嵌套ViewGroup
的布局中,如果最内层的Button
视图处理了触摸事件,那么外层的LinearLayout
或RelativeLayout
等ViewGroup
将不会再收到该事件。 onTouch
监听器优先于onClick
监听器 :如果为一个视图同时设置了OnTouchListener
和OnClickListener
,当触摸事件发生时,OnTouchListener
的onTouch
方法会首先被调用。只有当onTouch
方法返回false
时,才会触发OnClickListener
的onClick
方法。这是因为OnTouchListener
可以更细粒度地控制触摸事件的处理,例如在触摸按下、移动、抬起等不同阶段进行不同的操作。ViewGroup
的拦截机制影响优先级 :ViewGroup
可以通过重写onInterceptTouchEvent
方法来决定是否拦截事件。如果ViewGroup
拦截了事件(onInterceptTouchEvent
返回true
),则事件不会再分发给子视图,而是由ViewGroup
自身处理。这种机制使得ViewGroup
能够在必要时优先处理事件,例如在实现滑动菜单等功能时,ViewGroup
可以拦截触摸事件来处理滑动操作,而不会将事件传递给菜单中的子视图。
5.2.2 事件冲突解决
当多个视图对同一事件都有处理需求时,可能会出现事件冲突。常见的事件冲突场景包括:
-
滑动冲突 :例如,在一个包含可滑动列表(如
RecyclerView
)和可滑动的ViewGroup
(如实现侧滑菜单的DrawerLayout
)的布局中,如果用户在列表区域进行滑动操作,可能会与DrawerLayout
的滑动操作产生冲突。解决滑动冲突的方法通常有以下几种:- 根据滑动方向判断 :在
onInterceptTouchEvent
方法中,通过计算触摸事件的滑动方向来决定是否拦截事件。例如,如果检测到用户是水平滑动,且当前视图是DrawerLayout
,则拦截事件以处理侧滑菜单的展开和关闭;如果是垂直滑动,则不拦截,让RecyclerView
处理列表的滚动。 - 根据触摸位置判断 :根据触摸点的位置来决定事件的处理。例如,在
DrawerLayout
中,可以设置一个边缘区域(如左侧或右侧的一定宽度范围),当触摸点在这个区域内时,DrawerLayout
拦截事件处理侧滑;当触摸点在其他区域时,将事件传递给子视图。
- 根据滑动方向判断 :在
-
点击冲突 :当多个可点击的视图重叠时,可能会出现点击冲突。例如,一个
Button
视图覆盖在另一个ImageView
视图之上,且两个视图都设置了点击监听器。解决点击冲突的方法可以是:-
调整视图的层级关系 :通过设置视图的
z
轴坐标(在 Android 5.0 及以上版本支持)或在布局文件中调整视图的顺序,确保期望首先响应点击事件的视图处于上层。 -
在事件处理方法中进行判断 :在视图的
onTouchEvent
方法中,根据触摸点的位置和视图的边界等信息,判断当前触摸事件是否应该由该视图处理。例如,可以计算触摸点是否在Button
的边界范围内,如果是,则由Button
处理事件;否则,将事件传递给下层的ImageView
。
-
通过合理地利用事件处理的优先级规则和采用有效的冲突解决方法,开发者可以确保 Android 应用的视图交互体验更加流畅和符合用户预期。