在界面上点击按钮时,想想安卓是如何找到能响应事件的顶层View的?
如果给你坐标x、y, 你能找到对应的顶层View么?
首先安卓布局根节点是DecorView,并呈现为多叉树结构; 每个顶层View都是一个叶节点;
需求:手指在界面上滑动时显示对应的顶层View。
Activity显示在屏幕上时, 以DecorView为根节点并呈现多叉树数据结构;
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity"
>
<TextView
android:id="@+id/tv_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
/>
<Button
android:id="@+id/btn_ok"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginTop="10dp"
android:text="按钮"
/>
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#00ff00"
>
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center"
android:text="测试"
android:background="#ff0000"
/>
</FrameLayout>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="100dp"
android:text="控件信息"
/>
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:layout_marginLeft="10dp"
/>
</LinearLayout>
背景知识:
安卓事件分发时从最后一个子节点开始传递事件, 原因是什么呢?
ViewGroup.java的dispatchTouchEvent函数在遍历子View时, 是从最后一个子View开始分发事件的, 详见第2629行;
常用安卓ViewGroup为LinearLayout、FrameLayout和RelativeLayout, 如果当前ViewGroup是LinearLayout从前向后遍历也行; 但如果是FrameLayout或RelativeLayout就必须从后向前遍历,因为后边的子View可以覆盖前面的子View;
对于Android界面可以从DecorView开始查找, 如果坐标x,y在控件矩形范围内则继续向下递归,为了找到叶节点(即最上面的View)需要深度优先;
public class MainActivity extends AppCompatActivity {
private TextView mTvInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvInfo = findViewById(R.id.tv_info);
}
@Override public boolean onTouchEvent(MotionEvent event) {
showWidgetInfo(event.getX(), event.getY());
return super.onTouchEvent(event);
}
private void showWidgetInfo(float x, float y) {
View view = getViewByPosition(getWindow().getDecorView(), (int)x, (int)y);
if (view != null) {
mTvInfo.setText(view.toString());
} else {
mTvInfo.setText("没找到匹配的View");
}
}
private View getViewByPosition(View view, int x, int y) {
if (view == null) {
return null;
}
int[] location = new int[2];
view.getLocationInWindow(location);
int left = location[0];
int top = location[1];
int right = left + view.getWidth();
int bottom = top + view.getHeight();
if (view instanceof ViewGroup) { //当前是ViewGroup容器
int childCount = ((ViewGroup)view).getChildCount();
//深度优先, 从最后一个子节点开始遍历,如果找到则返回。 先递归判断子View
if (childCount > 0) {
for (int i = childCount - 1; i >= 0; i--) {
View topView = getViewByPosition(((ViewGroup)view).getChildAt(i), x, y);
if (topView != null) {
return topView;
}
}
}
//子View都没找到匹配的, 再判断自己
if (left < x && top < y && right > x && bottom > y) {
return view; //当前ViewGroup就是顶层View
} else {
return null; //没找到匹配的
}
} else { //当前是View
if (left < x && top < y && right > x && bottom > y) {
return view; //当前ViewGroup就是顶层View
} else {
return null; //没找到匹配的
}
}
}
}