Android UI實(shí)時(shí)預(yù)覽和編寫的各種技巧
一、啰嗦
之前有讀者反饋說(shuō),你搞這個(gè)所謂的最佳實(shí)踐,每篇文章最后就給了一個(gè)庫(kù),感覺(jué)不是很高大上。其實(shí),我在寫這個(gè)系列之初就有想過(guò)這個(gè)問(wèn)題。我的目的是:給出最實(shí)用的庫(kù)來(lái)幫助我們開發(fā),并且盡可能地說(shuō)明這個(gè)庫(kù)是如何編寫的,希望讓初創(chuàng)公司的程序員少寫點(diǎn)給后人留坑的代碼(想必大家對(duì)此深有體會(huì))。
我之前給出的庫(kù)都是很簡(jiǎn)單基礎(chǔ)的,基本是一看就懂(但足夠精妙),如果以后的文章涉及到了復(fù)雜的庫(kù),我會(huì)專門附加一篇庫(kù)的講解文。
如果一個(gè)庫(kù)的原理你知道,此外這個(gè)庫(kù)很容易擴(kuò)展和維護(hù),而且它還用到了很多最佳實(shí)踐的經(jīng)驗(yàn),你為什么不去試試呢?程序的意義在于把前人的優(yōu)秀思維和豐富經(jīng)驗(yàn)記錄下來(lái),讓使用者可以輕易地站在巨人的肩膀上。它的意義甚至堪比于將祖先的智慧通過(guò)DNA遺傳給我們,它是一種顛覆性的存在。如果我僅僅是分享自己在實(shí)踐中獲得的很多經(jīng)驗(yàn),這就不是程序,而是教育!
令人遺憾的是,我只能將很多有章可循的東西包裝為庫(kù),而調(diào)試UI這種雜亂無(wú)章的技巧只能通過(guò)文章來(lái)記錄,故產(chǎn)生了此文。
二、需求
有很多初學(xué)者都聽到前輩們說(shuō)Android Studio(下文簡(jiǎn)稱為as)的布局實(shí)時(shí)預(yù)覽很強(qiáng)大,但是當(dāng)我們真正使用as后就會(huì)發(fā)現(xiàn)很多界面在預(yù)覽時(shí)是這樣的:
或者是這樣的
甚至是這樣的:
這時(shí)候誰(shuí)再和我講as可以讓你實(shí)時(shí)地編寫UI,我就要和誰(shuí)拼命了。(┬_┬)
其實(shí)這個(gè)不是as的錯(cuò),而是開發(fā)者(包括google的開發(fā)人員)的錯(cuò)。因?yàn)楹芏嚅_發(fā)者不注重實(shí)時(shí)的ui顯示,一切都是以真機(jī)運(yùn)行的結(jié)果做評(píng)判標(biāo)準(zhǔn),從而產(chǎn)生了很多無(wú)法預(yù)覽,但能運(yùn)行的界面。在很多項(xiàng)目中,一個(gè)原本可以一秒內(nèi)看到的效果,最終需要漫長(zhǎng)的過(guò)程(編譯->運(yùn)行->安裝->顯示)才能被我們看到。我不得不說(shuō)這是反人類的,大大降低了Android程序員的開發(fā)效率,破壞了開發(fā)的心情(我是很注重開發(fā)心情的),讓as強(qiáng)大的預(yù)覽功能變得形同虛設(shè)。那么,既然官方不作為,只有我們自己來(lái)!下面就來(lái)說(shuō)說(shuō)如何讓自己的UI可實(shí)時(shí)調(diào)試的方案和技巧。
三、原則與技巧
3.0 指導(dǎo)性原則
將一次性的屬性放入xml中,將需要根據(jù)程序運(yùn)行產(chǎn)生變化的屬性放入java代碼中。
得益于布局文件的可預(yù)覽性(即使某個(gè)控件不可預(yù)覽,我們也應(yīng)該讓其支持預(yù)覽,下文會(huì)給出方案),我們可以大膽的編寫xml布局,而不用擔(dān)心后期維護(hù)難以定位的問(wèn)題。僅將動(dòng)態(tài)變化的東西放入java代碼中,就可以讓可變和不可變的代碼進(jìn)行分離,從而在本質(zhì)上趨于設(shè)計(jì)模式原則,在以后的編寫過(guò)程中你將會(huì)發(fā)現(xiàn)代碼自動(dòng)產(chǎn)生了很多優(yōu)化的空間,可讀性也增強(qiáng)了很多。
3.1 少用merge標(biāo)簽
很多文章都說(shuō)為了避免層級(jí)加深請(qǐng)用merge標(biāo)簽,但是我這里卻說(shuō)少用它。原因有兩點(diǎn): 1. merge標(biāo)簽會(huì)讓布局中各個(gè)元素的關(guān)系錯(cuò)亂,無(wú)法準(zhǔn)確的顯示ui位置(預(yù)覽時(shí))。 2. 在merge標(biāo)簽中會(huì)失去as自動(dòng)的代碼提示功能,讓編寫變得困難。
這兩點(diǎn)對(duì)于UI的實(shí)時(shí)預(yù)覽是極為致命的,所以推薦先用linearLayout等viewgroup做根布局,等編寫完畢了后再用merge來(lái)代替。我倒不是說(shuō)merge標(biāo)簽不好,merge標(biāo)簽的設(shè)計(jì)思路是很棒的,我只是想指出其問(wèn)題。可惜的是,這兩個(gè)問(wèn)題目前沒(méi)什么其他的好的解決方案了,只能等官方改進(jìn)IDE和增加tools的功能吧。
【吐槽】
一個(gè)很棒的merge標(biāo)簽被這兩個(gè)因素弄的很別扭,真是令人傷心,和它同病相憐的還有tools這個(gè)命名空間。
3.2 多用tools的屬性
xmlns:tools="
舉個(gè)例子: 這是我們之前的一個(gè)寫法,把textView的text屬性用android:來(lái)標(biāo)識(shí)。如果我們希望這個(gè)textview的文字在代碼中實(shí)時(shí)控制,默認(rèn)是沒(méi)文字怎么辦?這就需要tools的幫助了。 把第一行的android替換為tools這樣既可以能在預(yù)覽中看到效果,又不會(huì)影響代碼實(shí)際運(yùn)行的結(jié)果。因?yàn)樵趯?shí)際運(yùn)行的時(shí)候被tools標(biāo)記的屬性是會(huì)被忽略的。你完全可以理解為它是一個(gè)測(cè)試環(huán)境,這個(gè)測(cè)試環(huán)境和真實(shí)環(huán)境是完全獨(dú)立的,不會(huì)有任何影響。 【吐槽】 tools標(biāo)簽不支持代碼提示,而且自己的屬性也不能提示,全是靠自己記憶,或者先用android來(lái)代替,然后替換android為tools。這么長(zhǎng)時(shí)間以來(lái),google貌似一直沒(méi)管它,這也印證了google程序員也是不怎么愛實(shí)時(shí)預(yù)覽布局的人。 3.3 用tools來(lái)讓listview支持實(shí)時(shí)預(yù)覽 在之前的代碼中,我們總是這樣寫listview,然后腦補(bǔ)一下item放入的樣子。 現(xiàn)在我們可以利用tools來(lái)預(yù)覽item被放入的樣子了,就像這樣: 是不是好了很多呢。 利用tools的這兩個(gè)屬性可以讓我們不用盲寫UI了,也可以給設(shè)計(jì)一個(gè)很直觀的展示。 3.4 利用drawableXXX屬性來(lái)做有圖文的控件 textview和其子類都擁有drawableLeft、drawableRight等屬性,通過(guò)這些屬性可以讓我們很方便的做出有圖文控件。drawablePadding可以設(shè)置圖文之間的間距,但可惜沒(méi)有drawableLeftPadding之類的屬性。 比如我們要做一個(gè)兩邊有icon,文字居中的控件: 這時(shí)如果想調(diào)整文字位置,只需要修改gravity的值即可。 我們常見的這種(文字+箭頭)的控件就可以按照如下方式進(jìn)行制作: 3.5 利用space和layout_weight做占位 有時(shí)候我們的需求很復(fù)雜,希望一個(gè)linearLayout中多個(gè)控件分散于兩邊,因?yàn)閘inearLayout內(nèi)部的控件只能按照順序依次排列,想要完成這個(gè)效果要用到space了。 再舉個(gè)常見的例子: 我們要做一個(gè)上面是viewpager,底部是tab欄的主頁(yè)面。這種頁(yè)面如果僅僅用linearLayout是沒(méi)辦法做的,但如果用了layout_weight就可以很方便的完成。 關(guān)鍵代碼:
<TextView
android:text="Footer"
android:layout_width="wrap_content"
android:layout_height="100dp"
/>
<TextView
tools:text="Footer"
android:layout_width="wrap_content"
android:layout_height="100dp"
/>
<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listheader="@layout/demo_header"
tools:listitem="@layout/demo_item"
/>
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="50dp"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical|center_horizontal"
android:drawableLeft="@drawable/demo_tab_home_selector"
android:drawableRight="@drawable/demo_tab_home_selector"
android:drawablePadding="10dp"
android:text="ddd"
android:textSize="20sp"
/>
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="60dp"
android:padding="16dp"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical"
android:drawableRight="@drawable/icon_arrow"
android:drawablePadding="10dp"
android:text="設(shè)置菜單"
android:textSize="20sp"
/>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="12dp"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:gravity="center"
android:text="Header"
android:textSize="40sp"
/>
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/tab_icon_home"
/>
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<kale.uidemo.ExViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
/>
<kale.uidemo.ExTabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
android:layout_height="0dp"
android:layout_weight="1.0"
3.6 修改原生控件來(lái)支持實(shí)時(shí)預(yù)覽
上面也說(shuō)到了,很多Android的原生控件都沒(méi)為實(shí)時(shí)預(yù)覽做優(yōu)化,更不要說(shuō)第三方的了。在最近的項(xiàng)目中我就遇到了用tabLayout做主界面tab欄的需求。但是google設(shè)計(jì)的tablayout的耦合性太高了,它依賴于一個(gè)viewpager,而viewpager又依賴于adapter,adapter又依賴于數(shù)據(jù)。所以完全沒(méi)辦法獨(dú)立調(diào)試一個(gè)tablayout的樣子。因此,我修改了它的代碼,讓其支持了布局的實(shí)時(shí)預(yù)覽。主要就是加入了下面這段代碼:
private void preview(Context context, TypedArray a) {
final String tabStrArr = a.getString(R.styleable.ExTabLayout_tools_tabStrArray);
final String[] tabRealStrArr = getTabRealStrArr(tabStrArr);
ViewPager viewPager = new ViewPager(context);
viewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return tabRealStrArr.length;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public CharSequence getPageTitle(int position) {
return tabRealStrArr[position];
}
});
viewPager.setCurrentItem(0);
this.setupWithViewPager(viewPager);
}
你不是要viewpager么,我就給你viewpager。你不是要adapter么,我就給你adapter。你還要數(shù)據(jù),好我也給你數(shù)據(jù)。值得注意的是,如果你這塊代碼是為了實(shí)時(shí)預(yù)覽用,不想對(duì)真實(shí)的代碼做任何影響,那么請(qǐng)務(wù)必用到isInEditMode()這個(gè)方法,比如上面的代碼是這么調(diào)用的:
// preview
if (isInEditMode()) {
preview(context, a);
}
現(xiàn)在來(lái)看看效果吧:
這種修改原生控件支持預(yù)覽的做法沒(méi)什么高深的,大家可以用類似的思路去改造那些難以預(yù)覽的控件。
3.7 通過(guò)插件來(lái)進(jìn)行動(dòng)態(tài)預(yù)覽
我們都知道as的布局預(yù)覽只支持靜態(tài)預(yù)覽,我們不能對(duì)預(yù)覽界面進(jìn)行交互,這樣就無(wú)法測(cè)試滑動(dòng)效果和點(diǎn)擊效果了。所以我找到了jimu mirror這個(gè)插件來(lái)支持動(dòng)態(tài)預(yù)覽。啟動(dòng)mirror后,它會(huì)在你的手機(jī)上安裝一個(gè)apk,這個(gè)apk展示的就是你當(dāng)前的布局頁(yè)面,mirror會(huì)監(jiān)聽xml文件的改動(dòng),如果xml文件發(fā)生了變化,那么它就能立刻刷新布局。下面來(lái)展示下我是如何在它的支持下預(yù)覽viewpager的。
1. 首先在viewpager中加入這段代碼
private void preview(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExViewPager);
List<View> viewList = new ArrayList<>();
int layoutResId;
if ((layoutResId = a.getResourceId(R.styleable.ExViewPager_tools_layout0, 0)) != 0) {
viewList.add(inflate(context, layoutResId, null));
}
if ((layoutResId = a.getResourceId(R.styleable.ExViewPager_tools_layout1, 0)) != 0) {
viewList.add(inflate(context, layoutResId, null));
}
if ((layoutResId = a.getResourceId(R.styleable.ExViewPager_tools_layout2, 0)) != 0) {
viewList.add(inflate(context, layoutResId, null));
}
if ((layoutResId = a.getResourceId(R.styleable.ExViewPager_tools_layout3, 0)) != 0) {
viewList.add(inflate(context, layoutResId, null));
}
if ((layoutResId = a.getResourceId(R.styleable.ExViewPager_tools_layout4, 0)) != 0) {
viewList.add(inflate(context, layoutResId, null));
}
a.recycle();
setAdapter(new PreviewPagerAdapter(viewList));
}
/**
* @author Jack Tony
* 這里傳入一個(gè)list數(shù)組,從每個(gè)list中可以剝離一個(gè)view并顯示出來(lái)
* @date :2014-9-24
*/
public static class PreviewPagerAdapter extends PagerAdapter {
private List<View> mViewList;
public PreviewPagerAdapter(List<View> viewList) {
mViewList = viewList;
}
@Override
public int getCount() {
return mViewList.size();
}
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == arg1;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mViewList.get(position) != null) {
container.removeView(mViewList.get(position));
}
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
container.addView(mViewList.get(position), 0);
return mViewList.get(position);
}
}
上面的工作是為xml中設(shè)置viewpager中頁(yè)面的layout做支持,以達(dá)到預(yù)覽的作用。
2. 編寫xml布局文件
<kale.uidemo.ExViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:scrollbars="none"
app:scrollable="true"
app:tools_layout0="@layout/demo_fragment01"
app:tools_layout1="@layout/demo_fragment02"
app:tools_layout2="@layout/demo_fragment01"
/>
最后運(yùn)行插件即可看到效果:
四、快速預(yù)覽插件
上文提到了利用jimu mirror來(lái)做UI的實(shí)時(shí)預(yù)覽,更多的預(yù)覽技巧可以去他們的網(wǎng)站進(jìn)行瀏覽。mirror做的是實(shí)時(shí)替換靜態(tài)的xml文件,讓開發(fā)者可以在真機(jī)中看到UI界面,感興趣的朋友可以去試用體驗(yàn)版本的mirror。我在體驗(yàn)后感受到了它的強(qiáng)大和便捷,因?yàn)轶w驗(yàn)就幾十天,所以我不得不成為了付費(fèi)用戶。其中最令人喜愛的是,他支持tools標(biāo)簽的屬性并且支持力度強(qiáng)于as的實(shí)時(shí)預(yù)覽器。
與jimu mirror類似的,還有jrebel。這個(gè)東西更加強(qiáng)大,它做的不僅僅是讓UI界面實(shí)時(shí)刷新,它甚至做到了讓你更改java代碼后就能實(shí)時(shí)替換apk中的類文件,達(dá)到應(yīng)用實(shí)時(shí)刷新,我認(rèn)為它是采用了熱替換技術(shù)。官網(wǎng)的介紹是:Skip build, install and run,因此它可以節(jié)約我們很多很多的時(shí)間,它的效果也十分不錯(cuò)。
jrebel和mirror的側(cè)重點(diǎn)是不同的,它注重縮短應(yīng)用整體的調(diào)試時(shí)間,走的仍舊是真機(jī)出結(jié)果的路線。而mirror目的是讓開發(fā)者能實(shí)時(shí)預(yù)覽UI,走的是UI獨(dú)立測(cè)試的路線。總體來(lái)說(shuō)這兩款插件都挺不錯(cuò)的,這簡(jiǎn)直是給官方打臉啊。但因?yàn)閖rebel太貴了,所以我還是推薦大家用mirror。
五、總結(jié)
這篇文章確實(shí)挺長(zhǎng)的,也花了很多功夫。我仍舊覺(jué)得官方在設(shè)計(jì)和優(yōu)化IDE上程序員思維太重,給開發(fā)者帶來(lái)的便利還是太少。tools標(biāo)簽一直沒(méi)代碼提示、官方的控件的可預(yù)覽性不友好等問(wèn)題也使得開發(fā)者很難快速地進(jìn)行UI調(diào)試。在如今Android世界MVP、MVVM等模式大行其道的今天,UI獨(dú)立測(cè)試變得尤為重要,我不希望大家每次調(diào)試UI還得安裝運(yùn)行一遍apk,更加不希望看到as的實(shí)時(shí)預(yù)覽功能變成雞肋。
總之,感謝大家閱讀到最后,如果你有其他的UI調(diào)試技巧請(qǐng)指出,如果你覺(jué)得本文提出的技巧有用,那么請(qǐng)嘗試。
祝愿大家,雙十一快樂(lè)~
相關(guān)文章
Android開發(fā)自定義TextView省略號(hào)樣式的方法
這篇文章主要介紹了Android開發(fā)自定義TextView省略號(hào)樣式的方法,結(jié)合實(shí)例形式分析了Android文本控件TextView相關(guān)屬性與字符串操作技巧,需要的朋友可以參考下2017-10-10
Android代碼檢查規(guī)則Lint的自定義與應(yīng)用詳解
本文主要介紹了Android代碼檢查規(guī)則Lint的自定義與應(yīng)用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
Android自定義控件實(shí)現(xiàn)方向盤效果
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)方向盤效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04
Android實(shí)現(xiàn)顏色漸變動(dòng)畫效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)顏色漸變動(dòng)畫效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
Flutter Navigator路由傳參的實(shí)現(xiàn)
本文主要介紹了Flutter Navigator路由傳參的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
Android實(shí)現(xiàn)歌曲播放時(shí)歌詞同步顯示具體思路
歌曲播放時(shí)歌詞同步顯示,我們需要讀取以上歌詞文件的每一行轉(zhuǎn)換成成一個(gè)個(gè)歌詞實(shí)體,可根據(jù)當(dāng)前播放器的播放進(jìn)度與每句歌詞的開始時(shí)間,得到當(dāng)前屏幕中央高亮顯示的那句歌詞2013-06-06
Android自定義控件實(shí)現(xiàn)簡(jiǎn)單寫字板功能
這篇文章主要介紹了Android自定義控件實(shí)現(xiàn)簡(jiǎn)單寫字板功能的相關(guān)資料,需要的朋友可以參考下2016-04-04
Android在多種設(shè)計(jì)下實(shí)現(xiàn)懶加載機(jī)制的方法
這篇文章主要介紹了Android在多種設(shè)計(jì)下實(shí)現(xiàn)懶加載機(jī)制的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06














