最近看到了饿了么的一款很好用的工具,是一个各方人员(设计师、程序员、测试)都可以使用的调试工具。它可以作用于任何显示在屏幕上的 view,比如 Activity/Fragment/Dialog/PopupWindow 等等。
v1.0.14版本目前提供以下功能
- 移动屏幕上的任意 view,如果重复选中一个 view,将会选中其父 view
- 查看/修改常用控件的属性,比如修改 TextView 的文本内容、文本大小、文本颜色等等
- 如果你的项目里正在使用 Fresco 的 DraweeView 来呈现图片,那么 UETool 将会提供更多的属性比如图片 URI、默认占位图、圆角大小等等
- 你可以很轻松的定制任何 view 的属性,比如你想查看一些额外的业务参数
- 有的时候 UETool 为你选中的 view 并不是你想要的,你可以选择打开 ValidView,然后选中你需要的 View
- 显示两个 view 的相对位置关系
- 显示网格栅栏,方便查看控件是否对齐
接着我们来分析下它的源码
首先,我们要知道,通过UETool.showUETMenu()
显示UE对话框,通过UETool.dismissUETMenu()
关闭悬浮框。 悬浮框中一共有三个按钮,捕捉控件、相对位置、网格侧栏。
我们来看UETool.showUETMenu()
,会调用到UETool
的showMenu()
方法。
private boolean showMenu(int y) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(Application.getApplicationContext())) {
requestPermission(Application.getApplicationContext());
Toast.makeText(Application.getApplicationContext(), "After grant this permission, re-enable UETool", Toast.LENGTH_LONG).show();
return false;
}
}
if (uetMenu == null) {
uetMenu = new UETMenu(Application.getApplicationContext(), y);
}
uetMenu.show();
return true;
}
这里,如果是大于等于Android M 并且没有悬浮框权限,那么会提示打开权限后再重试。
如果权限正常,会调用UETMenu.show()
。
public void show() {
try {
windowManager.addView(this, getWindowLayoutParams());
} catch (Exception e) {
e.printStackTrace();
}
}
这里,会通过windowManager将UETMenu自己添加到Window中。再来看UETMenu。
public class UETMenu extends LinearLayout {
//...
}
来看UETMenu的构造方法,有这么一段
inflate(context, R.layout.uet_menu_layout, this);
setGravity(Gravity.CENTER_VERTICAL);
this.y = y;
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
vMenu = findViewById(R.id.menu);
vSubMenuContainer = findViewById(R.id.sub_menu_container);
Resources resources = context.getResources();
subMenus.add(new UETSubMenu.SubMenu(resources.getString(R.string.uet_catch_view), R.drawable.uet_edit_attr, new OnClickListener() {
@Override
public void onClick(View v) {
open(TransparentActivity.Type.TYPE_EDIT_ATTR);
}
}));
subMenus.add(new UETSubMenu.SubMenu(resources.getString(R.string.uet_relative_location), R.drawable.uet_relative_position,
new OnClickListener() {
@Override
public void onClick(View v) {
open(TransparentActivity.Type.TYPE_RELATIVE_POSITION);
}
}));
subMenus.add(new UETSubMenu.SubMenu(resources.getString(R.string.uet_grid), R.drawable.uet_show_gridding,
new OnClickListener() {
@Override
public void onClick(View v) {
open(TransparentActivity.Type.TYPE_SHOW_GRIDDING);
}
}));
for (UETSubMenu.SubMenu subMenu : subMenus) {
UETSubMenu uetSubMenu = new UETSubMenu(getContext());
uetSubMenu.update(subMenu);
vSubMenuContainer.addView(uetSubMenu);
}
可以看到UETMEnu的布局是uet_menu_layout
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
tools:background="#fff">
<ImageView
android:id="@+id/menu"
android:layout_width="54dp"
android:layout_height="54dp"
android:src="@drawable/uet_menu" />
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/sub_menu_container"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:background="@drawable/uet_sub_menu_bg"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="15dp"
android:paddingRight="10dp"
android:translationX="-10000dp" />
</HorizontalScrollView>
</merge>
其中,subMenus是一个List<UETSubMenu.SubMenu>
的集合,可以看到,此处将三个SubMenu加入到subMenus中,这三个subMenu就是捕捉控件、相对位置、网格侧栏这三个按钮。
接着,会遍历subMenus,并将SubMenu传入到 新new的UETSubMenu这个View中,并将UETSubMenu添加到id为sub_menu_container的vSubMenuContainer中。
当我们点击捕捉控件、相对位置、网格侧栏这三个按钮时,会执行其回调, open(根据不同按钮传入不同的值);
private void open(@TransparentActivity.Type int type) {
Activity currentTopActivity = Util.getCurrentActivity();
if (currentTopActivity == null) {
return;
} else if (currentTopActivity.getClass() == TransparentActivity.class) {
currentTopActivity.finish();
return;
}
Intent intent = new Intent(currentTopActivity, TransparentActivity.class);
intent.putExtra(TransparentActivity.EXTRA_TYPE, type);
currentTopActivity.startActivity(intent);
currentTopActivity.overridePendingTransition(0, 0);
UETool.getInstance().setTargetActivity(currentTopActivity);
}
可以看到open方法里会启动TransparentActivity,并将targetActivity设置为currentTopActivity。我们来看TransparentActivity。
public class TransparentActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Util.setStatusBarColor(getWindow(), Color.TRANSPARENT);
Util.enableFullscreen(getWindow());
setContentView(R.layout.uet_activity_transparent);
vContainer = findViewById(R.id.container);
final BoardTextView board = new BoardTextView(this);
type = getIntent().getIntExtra(EXTRA_TYPE, TYPE_UNKNOWN);
switch (type) {
case TYPE_EDIT_ATTR:
EditAttrLayout editAttrLayout = new EditAttrLayout(this);
editAttrLayout.setOnDragListener(new EditAttrLayout.OnDragListener() {
@Override
public void showOffset(String offsetContent) {
board.updateInfo(offsetContent);
}
});
vContainer.addView(editAttrLayout);
break;
case TYPE_RELATIVE_POSITION:
vContainer.addView(new RelativePositionLayout(this));
break;
case TYPE_SHOW_GRIDDING:
vContainer.addView(new GriddingLayout(this));
board.updateInfo("LINE_INTERVAL: " + DimenUtil.px2dip(GriddingLayout.LINE_INTERVAL, true));
break;
default:
Toast.makeText(this, getString(R.string.uet_coming_soon), Toast.LENGTH_SHORT).show();
finish();
break;
}
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
params.gravity = BOTTOM;
vContainer.addView(board, params);
}
//...
}
TransparentActivity是一个透明的Activity,在这之中,根据传入的类别的不同,会执行不同的操作。
TYPE_EDIT_ATTR:
即点击捕捉控件按钮后传入,会创建EditAttrLayout,并添加到vContainer这个Activity的根布局中。
来看EditAttrLayout,这是一个继承自CollectViewsLayout的自定义View。主要来看其onAttachedToWindow()
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
try {
Activity targetActivity = UETool.getInstance().getTargetActivity();
WindowManager windowManager = targetActivity.getWindowManager();
Field mGlobalField = Class.forName("android.view.WindowManagerImpl").getDeclaredField("mGlobal");
mGlobalField.setAccessible(true);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
Field mViewsField = Class.forName("android.view.WindowManagerGlobal").getDeclaredField("mViews");
mViewsField.setAccessible(true);
List<View> views = (List<View>) mViewsField.get(mGlobalField.get(windowManager));
for (int i = views.size() - 1; i >= 0; i--) {
View targetView = getTargetDecorView(targetActivity, views.get(i));
if (targetView != null) {
traverse(targetView);
break;
}
}
} else {
Field mRootsField = Class.forName("android.view.WindowManagerGlobal").getDeclaredField("mRoots");
mRootsField.setAccessible(true);
List viewRootImpls;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
viewRootImpls = (List) mRootsField.get(mGlobalField.get(windowManager));
} else {
viewRootImpls = Arrays.asList((Object[]) mRootsField.get(mGlobalField.get(windowManager)));
}
for (int i = viewRootImpls.size() - 1; i >= 0; i--) {
Class clazz = Class.forName("android.view.ViewRootImpl");
Object object = viewRootImpls.get(i);
Field mWindowAttributesField = clazz.getDeclaredField("mWindowAttributes");
mWindowAttributesField.setAccessible(true);
Field mViewField = clazz.getDeclaredField("mView");
mViewField.setAccessible(true);
View decorView = (View) mViewField.get(object);
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) mWindowAttributesField.get(object);
if (layoutParams.getTitle().toString().contains(targetActivity.getClass().getName())
|| getTargetDecorView(targetActivity, decorView) != null) {
traverse(decorView);
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
这里会通过反射获取WindowManagerImpl的mGlobal变量,并通过mGlobal获取decorView,接着会调用traverse()
。
private void traverse(View view) {
if (UETool.getInstance().getFilterClasses().contains(view.getClass().getName())) return;
if (view.getAlpha() == 0 || view.getVisibility() != View.VISIBLE) return;
if (getResources().getString(R.string.uet_disable).equals(view.getTag())) return;
elements.add(new Element(view));
if (view instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) view;
for (int i = 0; i < parent.getChildCount(); i++) {
traverse(parent.getChildAt(i));
}
}
}
在这之中,会不断递归decorView,并把相关View加入到elements中。
然后,当我们点击任意View的时候,会调EditAttrLayout的triggerActionUp。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = event.getX();
lastY = event.getY();
break;
case MotionEvent.ACTION_UP:
mode.triggerActionUp(event);
break;
case MotionEvent.ACTION_MOVE:
mode.triggerActionMove(event);
break;
}
return true;
}
@Override
public void triggerActionUp(final MotionEvent event) {
final Element element = getTargetElement(event.getX(), event.getY());
if (element != null) {
targetElement = element;
invalidate();
if (dialog == null) {
dialog = new AttrsDialog(getContext());
dialog.setAttrDialogCallback(new AttrsDialog.AttrDialogCallback() {
@Override
public void enableMove() {
mode = new MoveMode();
dialog.dismiss();
}
@Override
public void showValidViews(int position, boolean isChecked) {
int positionStart = position + 1;
if (isChecked) {
dialog.notifyValidViewItemInserted(positionStart, getTargetElements(lastX, lastY), targetElement);
} else {
dialog.notifyItemRangeRemoved(positionStart);
}
}
@Override
public void selectView(Element element) {
targetElement = element;
dialog.dismiss();
dialog.show(targetElement);
}
});
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (targetElement != null) {
targetElement.reset();
invalidate();
}
}
});
}
dialog.show(targetElement);
}
}
从show()中我们可以看到,会创建一个Dialog,并调用dialog.show(targetElement);
。注意这里通过getTargetElement()获取Element。
protected Element getTargetElement(float x, float y) {
Element target = null;
for (int i = elements.size() - 1; i >= 0; i--) {
final Element element = elements.get(i);
if (element.getRect().contains((int) x, (int) y)) {
if (element != childElement) {
childElement = element;
parentElement = element;
} else if (parentElement != null) {
parentElement = parentElement.getParentElement();
}
target = parentElement;
break;
}
}
if (target == null) {
Toast.makeText(getContext(), getResources().getString(R.string.uet_target_element_not_found, x, y), Toast.LENGTH_SHORT).show();
}
return target;
}
这里就是从之前elements添加的数据中获取的了。
然后,可以看到dialog.setAttrDialogCallback
中,AttrsDialog.AttrDialogCallback的这个回调,就是enableMove、showValidViews、selectView的具体执行了。这里就是改变View的具体执行了。