本文會(huì)介紹一個(gè)幫助我們快速調(diào)試UI參數(shù)的插件開發(fā)過(guò)程以及開發(fā)思路,可能需要一些簡(jiǎn)單的Idea平臺(tái)插件開發(fā)經(jīng)驗(yàn),希望對(duì)大家會(huì)有一些幫助。
插件介紹
插件基于Layout Inspector舅列,強(qiáng)化了這個(gè)工具,故取名Layout Master卧蜓。
使用方式同Layout Inspector帐要,呼出Android Studio(3.1以上)或Idea(2017.3以上)的Action面板,輸入Layout Master點(diǎn)擊即可弥奸,雙擊Property榨惠,支持修改的話會(huì)彈出Popup,同Layout Inspector一樣,每次Activity重啟了就需要再次運(yùn)行Layout Master才可赠橙。
插件效果如下(圖中僅演示了部分屬性修改效果伸蚯,支持很多屬性)
項(xiàng)目Github地址:https://github.com/wuapnjie/LayoutMaster
為什么要做這個(gè)插件
我在平時(shí)的Android開發(fā)過(guò)程中,會(huì)經(jīng)常修改一些UI的參數(shù)简烤,比如padding剂邮,margin,color等等横侦,有時(shí)View是通過(guò)非XML代碼動(dòng)態(tài)注入的挥萌,很多參數(shù)設(shè)置在真機(jī)調(diào)試時(shí)才能看到(而且我是那種一定要看真機(jī)跑效果的人),所以很多時(shí)候效果不滿意就要改參數(shù)繼續(xù)看效果枉侧,設(shè)計(jì)師們也會(huì)經(jīng)常讓我們改一些UI上的參數(shù)引瀑,這樣每次都要重新編譯運(yùn)行一次代碼,或者Instant Run一下榨馁,項(xiàng)目小還好憨栽,項(xiàng)目一大,這個(gè)重新編譯運(yùn)行的時(shí)間成本就會(huì)很大翼虫,大大降低了開發(fā)效率屑柔。于是我決定開發(fā)這個(gè)插件,快速看到UI參數(shù)改變的效果掸宛。
插件的簡(jiǎn)單原理介紹
不同于React Native和Flutter這些框架實(shí)現(xiàn)的熱加載(哈哈,其實(shí)我也不知道這些框架怎么實(shí)現(xiàn)的)招拙,這個(gè)插件對(duì)View的參數(shù)實(shí)時(shí)設(shè)置都是通過(guò)Java反射調(diào)用View自身的setXXX()方法實(shí)現(xiàn)的,所以只能看效果别凤,代碼本質(zhì)上沒(méi)有改變,需要達(dá)到滿意效果后再去修改规哪,但這還是大大節(jié)省了時(shí)間求豫,至少對(duì)我來(lái)說(shuō)是由缆。
那要怎么樣做到從電腦端(IDE端)調(diào)用APP上View的setXXX()方法呢?很簡(jiǎn)單均唉,讓手機(jī)與電腦之間進(jìn)行一個(gè)Socket長(zhǎng)連接,定義一些命令協(xié)議舔箭,就可以實(shí)現(xiàn)電腦端對(duì)手機(jī)端的控制罩缴。
實(shí)現(xiàn)思路與過(guò)程
最初的思考
首先要實(shí)現(xiàn)想要的功能,第一步就是建立手機(jī)端與電腦端的Socket長(zhǎng)連接并拿到關(guān)于Activity的View Hierarchy和View的Properties箫章。這樣的功能我在兩個(gè)地方看到過(guò),一個(gè)是Facebook強(qiáng)大的調(diào)試框架Stetho檬寂,另外一個(gè)就是Android Studio自帶的Layout Inspector终抽。這兩個(gè)工具都與手機(jī)端建立了一個(gè)Socket長(zhǎng)連接桶至,建立了自己的通信協(xié)議。下面我會(huì)簡(jiǎn)單介紹一下兩者的區(qū)別镣屹,并解釋了為什么我選擇基于Layout Inspector做一個(gè)插件,而不是基于Stetho做一個(gè)代碼擴(kuò)展女蜈。
Stetho
Stetho這個(gè)項(xiàng)目功能十分強(qiáng)大,不光可以看到View Hierarchy伪窖,還可以調(diào)試數(shù)據(jù)庫(kù)逸寓,監(jiān)測(cè)網(wǎng)絡(luò)等等惰许,實(shí)現(xiàn)上和我之前介紹的一樣,建立了一個(gè)Socket長(zhǎng)連接汹买,APP負(fù)責(zé)獲取需要的數(shù)據(jù)聊倔,通過(guò)Socket傳輸?shù)紺hrome DevTools,這里Chrome DevTools有一個(gè)開發(fā)API耙蔑,接收到特定的Json见妒,會(huì)進(jìn)行渲染顯示,在DevTools的操作也會(huì)Json格式包裝成特定的數(shù)據(jù)包發(fā)送給APP進(jìn)行操作甸陌。由于Stetho的代碼比較復(fù)雜须揣,我沒(méi)有對(duì)其深入研究,也不了解Chrome DevTools的API钱豁,但大致原理已經(jīng)介紹了耻卡,如果你感興趣或有什么想法,可以去研究研究牲尺。
Layout Inspector
同樣卵酪,Layout Inspector也是通過(guò)Socket長(zhǎng)連接來(lái)獲取APP的相關(guān)UI信息幌蚊,由于Idea的社區(qū)版代碼是開源的,而作為Android插件的Layout Inspector代碼也是開源溃卡,具體可以編譯Idea項(xiàng)目查看溢豆,代碼入口在android插件的AndroidRunLayoutInspectorAction.java
類中。
兩者差別
Stetho的Socket連接相關(guān)代碼是寫在它的庫(kù)中的瘸羡,需要調(diào)試的APP依賴這個(gè)項(xiàng)目漩仙,進(jìn)行一些配置,侵入性較強(qiáng)犹赖,但功能強(qiáng)大队他。而Layout Inspector則對(duì)代碼零侵入,那它是怎么實(shí)現(xiàn)Socket長(zhǎng)連接的呢冷尉?其實(shí)我們?cè)谡{(diào)試時(shí)漱挎,一直有一個(gè)長(zhǎng)連接連接著電腦,那就是ADB雀哨,ADB工具在電腦端建立了一個(gè)Socket服務(wù)端磕谅,連接著所有開啟了USB調(diào)試模式的手機(jī)客戶端,所以所有我們調(diào)試的應(yīng)用都可以使用Layout Inspector工具雾棺。
所以我選擇了基于Layout Inspector制作了一款插件膊夹,代碼零侵入,使用方便簡(jiǎn)單放刨,而且Android SDK中和Idea中已經(jīng)幫我做好了很多代碼工作,實(shí)現(xiàn)起來(lái)簡(jiǎn)單进统,接下來(lái)我會(huì)介紹。
Layout Inspector分析
要基于Layout Inspector做螟碎,勢(shì)必要對(duì)這個(gè)工具的實(shí)現(xiàn)過(guò)程有了解迹栓,這里我簡(jiǎn)單分析一下它的源碼,同時(shí)也會(huì)涉及到Android SDK中的一個(gè)類ViewDebug
克伊。
Action入口
做過(guò)Idea插件開發(fā)的同學(xué)肯定都知道Idea的Action系統(tǒng),很多我們進(jìn)行的快捷操作在Idea平臺(tái)中是一個(gè)個(gè)的Action
我們可以通過(guò)這個(gè)Action去快速找到它的入口類愿吹,上面也介紹了,在AndroidRunLayoutInspectorAction.java
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getProject();
assert project != null;
if (!AndroidSdkUtils.activateDdmsIfNecessary(project)) {
return;
}
AndroidProcessChooserDialog dialog = new AndroidProcessChooserDialog(project, false);
dialog.show();
if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
Client client = dialog.getClient();
if (client != null) {
new LayoutInspectorAction.GetClientWindowsTask(project, client).queue();
}
else {
Logger.getInstance(AndroidRunLayoutInspectorAction.class).warn("Not launching layout inspector - no client selected");
}
}
}
從 入口代碼中可以看出洗搂,我們要先選一個(gè)Process,也就是下面這個(gè)界面
Window選擇
之后會(huì)在Background執(zhí)行LayoutInspectorAction.GetClientWindowsTask
宇攻,這個(gè)Task會(huì)獲取當(dāng)前活躍的ClientWindow(也就是Android中的Window)倡勇,如果超過(guò)一個(gè)的話夸浅,會(huì)出現(xiàn)對(duì)話框讓我們選擇扔役,這里就不貼圖了,反正大家都用過(guò)亿胸。
Capture View
選擇了Window之后就會(huì)在Background執(zhí)行LayoutInspectorCaptureTask
,這個(gè)Task會(huì)獲取到需要顯示的View Hierarchy侈玄,View Properties以及一張BufferedImage(選擇Window的截圖),這些信息全部以二進(jìn)制的信息儲(chǔ)存在.li文件中
顯示
然后Layout Inspector自定義了一個(gè)FileEditor以支持.li文件的顯示突颊,也就是我們能看到View Tree和Properties Table的主界面。具體顯示細(xì)節(jié)可參考LayoutInspectorContext
類
Android SDK中的響應(yīng)
上面介紹了Layout Inspector在插件端的簡(jiǎn)單流程律秃,它想Android端要了Window信息,View的信息友绝,相關(guān)代碼都在HandleViewDebug
類,下面是這個(gè)類的一些結(jié)構(gòu)
也就是說(shuō)服務(wù)端發(fā)出了一些命令的包,那作為客戶端的Android是在哪里作出響應(yīng)的呢郭宝?經(jīng)過(guò)我的代碼查找,我在Android SDK中發(fā)現(xiàn)了一個(gè)DdmHandleViewDebug
類和ViewDebug
類
從兩個(gè)類的structure中就可以看出榄檬,Android端是在ViewDebug
這個(gè)類獲取各種信息的,具體代碼就不分析了鹿榜,大家感興趣可以研究研究海雪。
同時(shí)舱殿,這個(gè)類中有一個(gè)注解,叫ExportedProperty
/**
* This annotation can be used to mark fields and methods to be dumped by
* the view server. Only non-void methods with no arguments can be annotated
* by this annotation.
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportedProperty {
……
}
查看這個(gè)注解用的地方湾宙,可以發(fā)現(xiàn),所有Layout Inspector中顯示的Properties都被標(biāo)注了這個(gè)注解侠鳄。
通過(guò)這個(gè)注解我們可以給一些自定義的View暴露出想要顯示的屬性。
擴(kuò)展Layout Inspector
經(jīng)過(guò)上面的對(duì)Layout Inspector的分析伟恶,我們已經(jīng)足夠了解它并可以對(duì)其做擴(kuò)展了。Layout Inspector只能查看View Hierarchy和Properties博秫,它完全可以做更多的事情。
在HandleViewDebug
類中有一個(gè)方法invokeMethod
台盯,這個(gè)方法可以做到調(diào)用View的相關(guān)方法畏线,目前只支持primitive arguments的方法,很可惜寝殴,意味著我們不能改變TextView的text。
觸發(fā)的方法在Android SDK的ViewDebug
的invokeViewMethod
方法中市咽,可以看到是通過(guò)反射實(shí)現(xiàn)的施绎,view post出去的
/**
* Invoke a particular method on given view.
* The given method is always invoked on the UI thread. The caller thread will stall until the
* method invocation is complete. Returns an object equal to the result of the method
* invocation, null if the method is declared to return void
* @throws Exception if the method invocation caused any exception
* @hide
*/
public static Object invokeViewMethod(final View view, final Method method,
final Object[] args) {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Object> result = new AtomicReference<Object>();
final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
view.post(new Runnable() {
@Override
public void run() {
try {
result.set(method.invoke(view, args));
} catch (InvocationTargetException e) {
exception.set(e.getCause());
} catch (Exception e) {
exception.set(e);
}
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (exception.get() != null) {
throw new RuntimeException(exception.get());
}
return result.get();
}
接下來(lái)就好辦了,核心方法Idea和Android SDK都為我們提供好了俱尼,我們只需要構(gòu)建我們的插件UI遇八,寫入View的相關(guān)方法即可刃永。
由于我們需要對(duì)View的Property進(jìn)行操作揽碘,由于負(fù)責(zé)顯示View Properties的控件是私有的雳刺,所以這里我通過(guò)反射獲取了實(shí)例,并為其添加了一個(gè)雙擊鼠標(biāo)事件。
private var propertyTable: PTable
init {
val editorReflect = Reflect.on(layoutInspectorEditor)
val context = editorReflect.get<LayoutInspectorContext>("myContext")
propertyTable = context.propertiesTable
...
}
...
fun hook() {
propertyTable.addMouseListener(object : MouseAdapter() {
...
}
}
雙擊過(guò)后就是顯示一個(gè)Popup涌穆,不同的類型顯示不同的Popup雀久。
不支持動(dòng)畫的普通屬性
支持動(dòng)畫的屬性
顏色屬性
Enum類型的屬性
Bitwise類型的屬性
自定義的屬性
可以支持自定義View的自定義的屬性無(wú)疑是最棒的越庇,實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單涩惑,在介紹ViewDebug
類時(shí)桑驱,介紹了ExportedProperty
注解熬的,我們只需要在自定義的View中運(yùn)用這個(gè)注解就可以了悦析,并設(shè)置好setXXX()方法强戴,一個(gè)簡(jiǎn)單例子骑歹,注意這個(gè)category一定要為custom道媚,插件才會(huì)做出響應(yīng)最域,屬性名中帶有color會(huì)被認(rèn)為是顏色牺蹄。
public class ColorView extends TextView {
@ViewDebug.ExportedProperty(category = "custom", formatToHexString = true)
private int color = Color.BLACK;
@ViewDebug.ExportedProperty(category = "custom")
private int number = 0;
@ViewDebug.ExportedProperty(category = "custom")
private boolean needShowText = true;
public ColorView(Context context) {
super(context);
}
public ColorView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ColorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setColor(int color) {
this.color = color;
setBackgroundColor(color);
}
public void setNeedShowText(boolean needShowText) {
this.needShowText = needShowText;
if (!needShowText) {
setText("");
} else {
setText("" + number);
}
}
public void setNumber(int number) {
this.number = number;
setText("" + number);
}
}
之后的細(xì)節(jié)就不具體展開了沙兰,畢竟核心原理已經(jīng)介紹過(guò)了。插件代碼開源暑竟,感興趣的同學(xué)可以看看,不要噴我代碼寫的差就行。
結(jié)語(yǔ)
如果大家喜歡這個(gè)插件纱兑,可以在Android Studio或Idea的插件中心下載使用潜慎,喜歡這篇文章可以給個(gè)喜歡铐炫,有什么問(wèn)題可以評(píng)論或私信我。
請(qǐng)給個(gè)好評(píng),嘿嘿??
也可以直接在這里下載:https://github.com/wuapnjie/LayoutMaster/releases/tag/v1.0.0
安裝時(shí)不要解壓那個(gè)zip包
插件項(xiàng)目Github地址:https://github.com/wuapnjie/LayoutMaster 歡迎Star和PR
希望這篇文章可以對(duì)你有什么幫助优妙,我也會(huì)繼續(xù)努力~