做一個(gè)幫你快速調(diào)試UI參數(shù)的Android插件

本文會(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才可赠橙。

image

插件效果如下(圖中僅演示了部分屬性修改效果伸蚯,支持很多屬性)


LayoutMaster

項(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

image

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

image

同樣卵酪,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

image

我們可以通過(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è)界面

image

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文件中

image

顯示

然后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)

image

也就是說(shuō)服務(wù)端發(fā)出了一些命令的包,那作為客戶端的Android是在哪里作出響應(yīng)的呢郭宝?經(jīng)過(guò)我的代碼查找,我在Android SDK中發(fā)現(xiàn)了一個(gè)DdmHandleViewDebug類和ViewDebug

image

從兩個(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è)注解侠鳄。

image

通過(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。

image

觸發(fā)的方法在Android SDK的ViewDebuginvokeViewMethod方法中市咽,可以看到是通過(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)畫的普通屬性
image
支持動(dòng)畫的屬性
image
顏色屬性
image
Enum類型的屬性
image
Bitwise類型的屬性
image
自定義的屬性

可以支持自定義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);
  }

}

image

之后的細(xì)節(jié)就不具體展開了沙兰,畢竟核心原理已經(jīng)介紹過(guò)了。插件代碼開源暑竟,感興趣的同學(xué)可以看看,不要噴我代碼寫的差就行。

結(jié)語(yǔ)

如果大家喜歡這個(gè)插件纱兑,可以在Android Studio或Idea的插件中心下載使用潜慎,喜歡這篇文章可以給個(gè)喜歡铐炫,有什么問(wèn)題可以評(píng)論或私信我。


image

請(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ù)努力~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末套硼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子反砌,更是在濱河造成了極大的恐慌,老刑警劉巖呆贿,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異同衣,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)浪秘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門抵碟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人疚宇,你說(shuō)我怎么就攤上這事”陂牛” “怎么了牌里?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵贪染,是天一觀的道長(zhǎng)杭隙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)攀涵,這世上最難降的妖魔是什么以故? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任怒详,我火速辦了婚禮,結(jié)果婚禮上昆烁,老公的妹妹穿的比我還像新娘静尼。我一直安慰自己鼠渺,他們只是感情好拦盹,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布掌敬。 她就那樣靜靜地躺著奔害,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芯杀。 梳的紋絲不亂的頭發(fā)上揭厚,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天裂明,我揣著相機(jī)與錄音,去河邊找鬼提岔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛荠瘪,可吹牛的內(nèi)容都是我干的赛惩。 我是一名探鬼主播坊秸,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼阶牍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了走孽?” 一聲冷哼從身側(cè)響起磕瓷,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎硕盹,沒(méi)想到半個(gè)月后瘩例,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垛贤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了聘惦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片善绎。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涂邀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出驹止,到底是詐尸還是另有隱情臊恋,我是刑警寧澤抖仅,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布环凿,位于F島的核電站智听,受9級(jí)特大地震影響到推,放射性物質(zhì)發(fā)生泄漏惕澎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一悔雹、第九天 我趴在偏房一處隱蔽的房頂上張望腌零。 院中可真熱鬧益涧,春花似錦闲询、人聲如沸扭弧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)厚满。三九已至,卻和暖如春遵馆,著一層夾襖步出監(jiān)牢的瞬間团搞,已是汗流浹背逻恐。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工复隆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挽拂,地道東北人亏栈。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像黎侈,于是被迫代替她去往敵國(guó)和親闷游。 傳聞我的和親對(duì)象是個(gè)殘疾皇子脐往,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容