你有想过自己动手写一个Layout Inspector吗?

14cd3b5bf852cea016e62b77355590b4.png

/   今日科技快讯   /

近日,在“华为无线创新产品与解决方案发布会暨MWC2022预沟通会”上,华为无线网络产品线副总裁、首席营销官甘斌就“创新永恒,共建5Gigaverse社会”进行主题发言,发布了华为无线最新产品与解决方案——TDD第三代Massive MIMO产品和FDD超宽带多天线系列产品。

/   作者简介   /

本篇文章来自我是技术男的投稿,文章主要分享了他开发的Android轻量级Layout Inspector工具的功能和使用,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

我是技术男的博客地址:

https://www.jianshu.com/u/2ce7b74b592b

/   效果图   /

99a14961dcd1d68bbdfc9c18e7948999.gif

layout inspector工具效果

/   为什么要做这个工具   /

如果有更好的轮子用,谁愿意造轮子。

在21年年初的时候,我被调到公司的另外一个项目,这个项目有几个特点:历史非常的悠久,使用的技术落后,项目工程复杂,代码量巨大,界面布局层次特别深。这么庞大的项目对于我这个“新人”来说,找个功能的实现,或者找某个view的点击事件是在哪实现的,或者某个view所属的layout是啥等等都很困难。

通过阅读项目的源码来找具体功能点是可行的(比如找某个view的点击事件在哪,大致步骤是先找到Activity进而找到布局layout文件,进而找到view的id,再去找这个id在什么地方设置了click事件),这种方法确实可行,但是效率低下。那有没有高效率的方法呢,我想到了android studio(以下简称as)自带的layout inspector工具,通过它可以知道界面中view的id及其他元素信息,通过view的id可以在as中全局搜索,从而定位到具体的layout和代码位置(这只是基于全局只存在一个唯一的id情况)。

当我使用as的layout inspector工具时候,现实给了我重重的一棒,不知道是我们app复杂的原因,as的layout inspector在检测中经常会导致app死掉(检测工具报oom导致app死掉),并且检测过程中经常会出现检测不到设备的问题或者一直处于检测中等问题(不可否认as后面的版本肯定会把这些问题都修好的)。

因此我就想自己是否能做一个这样的工具,这个工具可以具有以下特点:

  1. 直接在手机上显示view的属性:宽高,坐标,id值等等,可以显示点击事件/长按事件的具体代码位置,view控件的所属的layout文件名称和被inflate的位置,以及使用者可以自己扩展需要显示view的属性(比如自定义view的属性)

  2. 为开发人员提供服务

  3. 为设计人员提供服务(比如在与设计人员走查ui时,提高效率和准确性)

/   轻量级Layout Inspector工具都有哪些功能   /

菜单

菜单会显示在每个activity上的左上部分,是可以拖动的(若阻碍了当前界面操作)显示 菜单:点击它后,会把当前界面的所有view的边界,pading,margin显示出来,这时候文本变为 隐藏。这时候点击每个view都会显示view信息的界面,view的真正点击事件不会触发(若需要触发,再次点击该菜单,隐藏上面的界面),如下图:

87862f00563e3432f332dfcde96033e2.png

更多菜单

e10b4cc8e7db890fc782b692cff0c0f7.png


上图元素说明:

  • padding,margin显示样式说明屏幕的宽高

  • 当前activity,activity layout的信息 

  • 当前的fragment以及总共显示了多少fragment 

  • pading,margin等尺寸的单位:可以选择dp

  • pxViewGroup显示View检测器:点击是表面显示它的检测器, 点击否不显示 

  • 显示view margin:是绘制所有view的margin,否不绘制 

  • 显示view padding:是绘制所有view的padding,否不绘制

显示view的margin,padding

3e0d969b2b80c3b291c960da7fef5abe.png

view的margin,padding

17f13edab22f50edb596f92cf7d9f44b.png

margin

上图浅蓝色的箭头,代表view的margin,箭头的朝向分别代表不同方向的margin如left,right,top,bottom,箭头的长度越大代表margin值越大。

df126b42c3a5d9e42a7c4ae0c8a11e76.png

padding

上图浅蓝色的矩形框,代表view的padding,处于view的上部分代表padding top,下部分代表padding bottom,左部分代表padding left,右部分代表padding right。矩形框高度越大代表padding值越大。

红色线条,代表view的边界线。

显示view的属性

82b987eea873c54b7ca9e2feada7afbd.png

view的id,size,坐标,布局,click信息

上图元素说明:

  • 绿色框代表当前被检测的view 

  • 弹出的深蓝色框包含:控件属性和控件层级

  • 文本为 可点击 背景框为红色的项:代表该项是可以点击的

显示view的id,类名

view检查器:点击不现实该view的检查器

它主要用于:view层级出现覆盖的时,想查看最底层view的属性时,可以点击此项,不显示最上层的view的检查器。

所属布局名称,布局被inflate位置

这两项非常的有用,前者可以显示当前的view所属的layout名称,后者可以显示layout布局被inflate的位置(类名#方法名#行号),甚至可以在as的logcat中搜索LayoutInspector的tag,可以直接点击这个log跳转到具体代码位置。

是否设置onClickListener,onClickListener位置

这两项在查找view的onClickListener信息的时候非常有用,onClickListener位置包含 类名#方法名#行号 信息(可以在as的logcat中搜索LayoutInspector的tag,可以直接点击这个log跳转到具体代码位置)

是否设置onLongClickListener,onLongClickListener位置

这两项在查找view的onLongClickListener信息的时候非常有用,onLongClickListener位置同onClickListener位置。

显示size,layout_width,layout_height,坐标(x , y)

e3e1283375a8d59006a67a2f3ad1eec4.png

padding,margin,activity

显示view的padding,margin信息

(参考上图) 这两项也是非常有用,在和设计同学走查ui的时候,开发同学深有体会,明明是按设计的尺寸写的值,设计同学会觉得不对,这时候咱们就可以用数据来说话了。同时也为设计同学带来了方便,想检查哪个view的信息时候,自己就可以操作了。

动态修改view属性这是一个非常好用的功能  ,比如点击paddingTop这一项,会弹出一个 对话框,可以动态的修改paddingTop的值,这个功能可以提高 与设计同学走查ui的效率(不需要重复 在代码或布局文件中修改属性值,启动app查看效果这个过程),动态修改到合适的值后,再去代码或布局文件中修改,启动查即可)

对于TextView显示textcolor,和textsize

同样它们也可以适用动态修改view属性这个功能。

显示当前activity和activity layout信息

显示view的层级

e57597cbd7e274191f3aa004088eff9d.png

控件层级

上图元素说明:

绿色字体:代表当前被查看的view。

由于担心显示的view实在过多的原因,暂时只会把当前view的子view和它的兄弟view给显示出来,当前view的祖先view不会罗列这些信息。点击对应项的view会显示该view的属性和层级信息。

显示Dialog的信息

45d8fbfa6af3e741960fb8d99afdeb34.png

dialog

Dialog的处理要稍微特殊一些,为了简洁化,把 更多 菜单去掉了,其他的操作,显示的属性信息都与上面介绍的一样。

显示PopupWindow的信息

在这就不展示图片了,可以查看文章开始的gif图。

/   接入   /

工程根目录下的build.gradle文件添加下面内容:

buildscript {

    repositories {
        //replacemethod库
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        //replacemethod的类,主要对inflate,setOnclickListener等方法进行替换
        classpath "com.github.niuxiaowei:ReplaceMethod:1.0.0"
    }
}

allprojects {
    repositories {
        //LayoutInspector工具地址
        maven { url 'https://jitpack.io' }
    }
}

在app下的build.gradle文件添加下面内容apply plugin: 'ReplaceMethodPlugin'。

apply plugin: 'ReplaceMethodPlugin'

dependencies {
  debugImplementation 'com.github.niuxiaowei.LayoutInspector:layoutinspector:2.0.1'
  releaseImplementation 'com.github.niuxiaowei.LayoutInspector:layoutinspector-no-op:2.0.1'
}

replaceMethod {
    open = true //这里如果设置为false,则会关闭插桩
    openLog false
    replaceByMethods {
        //对inflate方法进行替换,用来显示view的layout被inflate的位置信息
        register {
            replace {
                invokeType "ins"
                className "android.view.LayoutInflater"
                methodName "inflate"
                desc "(int,android.view.ViewGroup)android.view.View"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.LayoutInflaterProxy"
                addExtraParams = true
            }
        }

        register {
            replace {
                invokeType "static"
                className "android.view.View"
                methodName "inflate"
                desc "(android.content.Context,int,android.view.ViewGroup)android.view.View"
                ignoreOverideStaticMethod true
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.LayoutInflaterProxy"
                addExtraParams  true
            }
        }

        register {
            replace {
                invokeType "ins"
                className "android.view.LayoutInflater"
                methodName "inflate"
                desc "(int,android.view.ViewGroup,boolean)android.view.View"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.LayoutInflaterProxy"
                addExtraParams =  true
            }
        }

        //若想对dialog进行检测,需要添加下面配置
        register {
            replace {
                invokeType "ins"
                className "android.app.Dialog"
                methodName "show"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.DialogProxy"
            }
        }

        //若想对PopupWindow进行检测,需要添加下面配置
        register {
            replace {
                invokeType "ins"
                className "android.widget.PopupWindow"
                methodName "showAsDropDown"
                desc "(android.view.View)"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.PopupWindowProxy"
            }
        }

        register {
            replace {
                invokeType "ins"
                className "android.widget.PopupWindow"
                methodName "showAsDropDown"
                desc "(android.view.View,int,int)"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.PopupWindowProxy"
            }
        }

        register {
            replace {
                invokeType "ins"
                className "android.widget.PopupWindow"
                methodName "showAsDropDown"
                desc "(android.view.View,int,int,int)"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.PopupWindowProxy"
            }
        }

        register {
            replace {
                invokeType "ins"
                className "android.widget.PopupWindow"
                methodName "showAtLocation"
                desc "(android.view.View,int,int,int)"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.PopupWindowProxy"
            }
        }

        //对setOnClickListener方法进行替换,在view检测器中显示click的位置信息
        register {
            replace {
                invokeType "ins"
                className "android.view.View"
                methodName "setOnClickListener"
                desc "(android.view.View\$OnClickListener)"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.OnClickListenerProxy"
                addExtraParams =  true
            }
        }

        //对setOnLongClickListener方法进行替换,在view检测器中显示click的位置信息
        register {
            replace {
                invokeType "ins"
                className "android.view.View"
                methodName "setOnLongClickListener"
                desc "(android.view.View\$OnLongClickListener)"
            }
            by {
                className = "com.mi.layoutinspector.replacemethod.OnClickListenerProxy"
                addExtraParams =  true
            }
        }

    }
}


扩展view的属性调用下面的方法,可以在检测器中显示扩展的view属性,具体可以参考demo中的例子。

LayoutInspector.INSTANCE.register(IViewAttributeCollector collector)


如下代码对TextView的文本进行动态修改(检测器显示的时候就会显示该项):

LayoutInspector.INSTANCE.register(new IViewAttributeCollector() {
            @Nullable
            @Override
            public List<ViewAttribute> collectViewAttributes(@NotNull View inspectView, @NotNull IViewInspector IViewInspector) {
                return null;
            }

            @Nullable
            @Override
            public ViewAttribute collectViewAttribute(@NotNull View inspectView, @NotNull IViewInspector IViewInspector) {
                if (inspectView instanceof TextView) {
                    ViewAttribute viewAttribute = new ViewAttribute("修改TextView内容", "点击进行修改", v -> {
                        IViewInspector.hideViewInfosPopupWindown();
                        TextView textView = (TextView) inspectView;
                        final EditText editText = new EditText(inspectView.getContext());
                        AlertDialog.Builder inputDialog =
                                new AlertDialog.Builder(inspectView.getContext());
                        inputDialog.setTitle("输入内容").setView(editText);
                        inputDialog.setPositiveButton("确定修改",
                                (dialog, which) -> {
                                    String msg = editText.getText().toString();
                                    if (!TextUtils.isEmpty(msg)) {
                                        textView.setText(msg);
                                    }
                                }).show();
                    });
                    return viewAttribute;
                }
                return null;

            }
        });

本工具的代码在github上,代码地址:

https://github.com/niuxiaowei/LayoutInspector

本工具还是用了一个对方法进行替换的工具,代码地址:

https://github.com/niuxiaowei/ReplaceMethod

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

再学一遍android:fitsSystemWindows属性

PermissionX 1.5发布,支持申请Android特殊权限啦

欢迎关注我的公众号

学习技术或投稿

74798a901ba07f7ec5a4d341f15bf4a5.png

f85774ef4038a4b7d7e1db76615903b8.png

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值