日常開發過程中,我們都會遇到這樣一種場景:我們寫出的 UI 效果在對接數據之前需要提前進行預覽,進而調整 UI 細節和排版問題。我們一般的做法是什么樣的?如果存在像 TextView 或者 ImageView 這種基礎控件,你是不是還在通過諸如 android:text="xxx"
和 android:src="@drawable/xxx"
的方式來測試和預覽UI效果?當然你肯定也會遇到這些“臟數據”給你帶來的困擾:測試的時候某些地方出現了本不該出現的數據,事后可能一拍腦門才發現,原來是布局中控件預覽數據沒有清除導致的。如果是 RecyclerView,在后台接口尚能測試的情況下,你是否又要自己生成“假數據”並手寫 Adapter 呢?這時候你不禁會問:有沒有一種方法,既能夠做到布局時預覽數據方便排版,又能夠在對接真實數據運行后動態替換和移除這些無關數據呢?
鐺鐺鐺鐺!Android 的 Tools attributes
應運而生。老規矩,我們先來看一個效果:
What?你在耍我嗎?這么簡單的列表拿出來干嘛?哈哈,客觀不要着急。這個並不難實現,倘若我說這里並沒有寫一行 Java 或者 Kotlin 代碼就實現了此效果,而只是在布局頁面預覽,你敢信嗎?上圖只是冰山一角,下面這張圖才是全貌:
下面會帶大家一步步實現上述功能,首先,讓我們從頭說起。
認識 Tools attributes
Tools attributes 即以 tools
開始的命名空間,舉個我們最常見到的例子:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
</android.support.constraint.ConstraintLayout>
大家肯定平時都會見到 tools:context=".XXXActivity
這個系統默認為我們生成的配置。一般來說,只有根視圖才能使用這個屬性,它指定了當前布局默認是與哪個 Activity 相關聯,使得布局能夠獲取到綁定 Activity 的一些信息,比如 Theme 等等,而且當你在布局中給子 View 添加 onClick
事件時,相應的方法代碼會插入到這個 Activity 中。
Android studio 支持很多在 XML 文件中以 tools 為命名空間的屬性,當構建 App 時這些屬性會被擦除,對 APK 的大小和運行時行為沒有任何影響,這也就是我們文章最初想要的結果。
細說 Tools attributes
在具體介紹 Tools attributes 之前,我們需要先了解如何引入 Tools 的命名空間並使用,很簡單,只需要在 XML 布局文件的根元素中添加即可:
<RootTag xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
這些工具屬性大概可以分為以下三類:
Error handling attributes
即錯誤和警告處理屬性。這類屬性常被用來規避被 lint 檢查出的一些錯誤提示以及警告。下面讓我們看一些常見的例子:
-
tools:ignore
主要用來忽略一些 lint 產生的警告信息,並支持一些屬性,例如:
<resources xmlns:tools="http://schemas.android.com/tools"> <string name="app_name">ConstraintSample</string> <string name="header_image_string" tools:ignore="MissingTranslation">header image</string> </resources>
這個對於 Android studio 升級到 3.0 以上的小伙伴來說應該是很常見了,如果我們項目中涉及到國際化支持,那么編譯器就會提示我們為每一種語言做適配,不能“厚此薄彼”,如果我們某些 string 只需要支持一種語言,只需要像上面那樣添加
tools:ignore="MissingTranslation
即可。相似的例子還可以在使用 ImageView 的時候看到:<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_person_off" tools:ignore="contentDescription" />
-
tools:targetApi
這個屬性的功能和 Java 代碼中的注解 @TargetApi 是一樣的:它指定了當前控件或元素支持的 API 級別,屬性值可以是 API Code 名或者 API 常數值,它支持一切屬性。如:我們都知道,
android:elevation
屬性是在 API 21 版本以上才支持的,所以我們可以通過以下代碼規避 lint 的警告:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:elevation="4dp" tools:targetApi="lollipop"/>
-
tools:locale
這個屬性主要用在
標簽內,用來指定當前資源中默認為哪種語言和區域,進而規避語言拼寫的檢測,比如你可以指定 values/strings.xml
文件的默認語言是西班牙語而不是英語:<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="es">
Resource shrinking attributes
即資源壓縮屬性。關於此類屬性的用法我們已經在之前的一篇文章帶你領略Android混淆的魅力一文中做過詳細講解,下面再簡單說明一下。
我們可以通過 tools:shrinkMode 和 tools:keep 屬性來分別指定資源壓縮的模式和需要保留的不被壓縮的資源 ,還可以通過 tools:discard 屬性來指定需要保留的資源,與 keep 功能類似:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="strict"
tools:keep="@layout/activity_video*,@layout/dialog_update_v2"
tools:discard="@layout/unused_layout,@drawable/unused_selector" />
下面就到本篇文章的重頭戲了,注意,前方高能來襲!
Design-time View Attributes
這就是我們先前效果圖中的重要功臣了,即:布局設計時的控件屬性。這類屬性主要作用於 View 控件,如上文所說的 tools:context 就是“成員”之一,下面我們來介紹其他重要成員。
在此之前,我們需要先揭開 tools 命名空間的另一層神秘面紗:tools: 可以替換任何以 android: 為前綴的屬性,並為其設置樣例數據(sample data)。當然,正如我們前面所說,tools 屬性只能在布局編輯期間有效,App真正運行后就毫無意義了,所以,我們就可以像下面這樣來在運行前預覽布局效果:
上圖對應的布局文件為:
Card_item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@android:color/white"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
tools:targetApi="m"
tools:ignore="UnusedAttribute">
<ImageView
android:id="@+id/card_item_avatar"
android:layout_width="38dp"
android:layout_height="38dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp"
app:layout_constraintVertical_bias="0.0"
tools:ignore="ContentDescription"
tools:srcCompat="@drawable/user_other"/>
<TextView
android:id="@+id/card_item_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="@+id/card_item_title"
app:layout_constraintEnd_toEndOf="@+id/card_item_title"
app:layout_constraintHorizontal_bias="0.0"
android:textSize="12sp"
android:textColor="@color/username_text_color"
android:layout_marginEnd="16dp"
android:paddingEnd="16dp"
tools:text="水月沐風" />
<TextView
android:id="@+id/card_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/title_text_color"
app:layout_constraintStart_toEndOf="@+id/card_item_avatar"
android:layout_marginStart="12dp"
app:layout_constraintTop_toBottomOf="@+id/card_item_username"
android:layout_marginTop="8dp"
android:maxLines="1"
tools:text="今天上海的夜色真美!"/>
<TextView
android:id="@+id/card_item_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@+id/card_item_avatar"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp"
app:layout_constraintVertical_bias="1.0"
android:maxLines="3"
android:ellipsize="end"
android:textColor="@color/content_text_color"
android:textStyle="normal"
app:layout_constraintEnd_toEndOf="@+id/card_item_bottom_border"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
app:layout_constraintHorizontal_bias="0.0"
tools:text="人生若只如初見,何事秋風悲畫扇..."/>
<ImageView
android:id="@+id/card_item_poster"
android:layout_width="0dp"
android:layout_height="200dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/card_item_content"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="16dp"
android:scaleType="centerCrop"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp"
app:layout_constraintVertical_bias="0.0"
tools:ignore="ContentDescription"
android:visibility="visible"
tools:src="@drawable/shanghai_night"/>
<View
android:id="@+id/card_item_bottom_border"
android:layout_width="0dp"
android:layout_height="2dp"
app:layout_constraintTop_toBottomOf="@+id/card_item_poster"
android:background="#ffededfe"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/card_item_date"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="16dp"
android:textColor="@color/date_text_color"
android:textSize="12sp"
tools:text="2019-08-10"/>
</android.support.constraint.ConstraintLayout>
通過上面代碼我們可以發現:通過 對 TextView 使用 tools:text
屬性代替 android:text
就可以實現文本具體效果的預覽,然而這項設置並不會對我們 App 實際運行效果產生影響。同理,通過將 tools:src
作用於 ImageView 也可以達到預覽圖片的效果。此外。我們還可以對其他以 android: 為前綴的屬性進行預覽而不影響實際運行的效果,例如:上面布局代碼中的底部分割線
<View
android:id="@+id/card_item_bottom_border"
android:layout_width="0dp"
android:layout_height="2dp"
android:visibility="gone"
tools:visibility="visible"
tools:layout_height="8dp"
app:layout_constraintTop_toBottomOf="@+id/card_item_poster"
android:background="#ffededfe"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"/>
如上所示,通過 tools:visibility
和 tools:layout_height
就可以僅在布局預覽情況下改變 View 的狀態和高度。雖然上述情況比較少用,但是希望大家也能夠知道,tools:
可以替代所有 android:
修飾的屬性。
下面再列舉一些其他會常用到的屬性。
-
tools:layout
這個屬性只能用於 fragment 控件中,如果我們的 activity 布局文件中聲明了
<fragment>
控件,我們就可以通過tools:layout=”@layout/fragment_main”
來在當前 activity 布局中預覽 fragment 中的布局效果。 -
tools:showIn
這個屬性就比較好玩了,它可以指定其他布局文件像
組件一樣在當前布局文件中使用和預覽 控件的實際效果。例如,我們 card_item_layout.xml
作為 showIn 的對象給show_in_layout.xml
布局使用,然后我就可以看到show_in_layout.xml
中如下效果:
-
tools:menu
這個屬性可以給當前布局預覽器的 Toolbar 添加多個菜單項,但僅限於布局文件的根節點元素。如:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" tools:menu="menu1,menu2" />
-
tools:maxValue | tools:minValue
這兩個屬性僅用於
,可以在預覽時指定其最大值和最小值: <NumberPicker xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/numberPicker" android:layout_width="match_parent" android:layout_height="wrap_content" tools:minValue="0" tools:maxValue="10" />
-
tools:listitem | tools:listheader | tools:listfooter | tools:listCount
下面來講一下列表相關組件的 tools 屬性。上面四個屬性僅用於
及其子類(如:ListView 和 RecyclerView)。然而,它們內部仍有一些使用限制: tools:listCount
僅用於 RecyclerView;tools:listheader
和tools:listfooter
僅限於 ListView;至於tools:listitem
屬性二者皆可用。之前的效果圖就是借助於此屬性:activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:background="#ffEBEBEF"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="0dp" android:layout_height="wrap_content" android:background="?attr/colorPrimary" android:theme="?attr/actionBarTheme" android:minHeight="?attr/actionBarSize" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:elevation="1dp" app:title="@string/app_name" app:layout_constraintTop_toTopOf="parent" app:layout_constraintHorizontal_bias="0.0"/> <android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" android:scrollbars="vertical" app:layout_constraintTop_toBottomOf="@+id/toolbar" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintVertical_bias="0.0" tools:listitem="@layout/card_item_layout"/> </android.support.constraint.ConstraintLayout>
sample data
即 樣本數據 功能,可以通過 @tools:sample
來使用該屬性,也屬於 design-time view attributes。但它並非只是一個屬性那么簡單,更應該算是一個“工具利器”,所以會將其單獨拿出來詳細介紹。這個工具是本年度 Google 大會上 Android 開發團隊特別介紹的一個新推屬性。它有什么用呢?用處大了!先前的布局預覽使用的數據都是我們直接在布局控件中注明或者在 strings.xml
文件中給出的,這一就會產生一些臟數據,不利於我們后期的處理。而有了 sample data,我們就可以對布局預覽器中的 “樣本數據”進行集中保存和管理了。
一、sample data 的使用
Android studio 已為我們提供了以下樣本數據,我可以直接拿來使用:
Attribute value | Description of placeholder data |
---|---|
@tools:sample/full_names |
Full names that are randomly generated from the combination of@tools:sample/first_names and @tools:sample/last_names . |
@tools:sample/first_names |
Common first names. |
@tools:sample/last_names |
Common last names. |
@tools:sample/cities |
Names of cities from across the world. |
@tools:sample/us_zipcodes |
Randomly generated US zipcodes. |
@tools:sample/us_phones |
Randomly generated phone numbers with the following format: (800) 555-xxxx . |
@tools:sample/lorem |
Placeholder text that is derived from Latin. |
@tools:sample/date/day_of_week |
Randomized dates and times for the specified format. |
@tools:sample/date/ddmmyy |
|
@tools:sample/date/mmddyy |
|
@tools:sample/date/hhmm |
|
@tools:sample/date/hhmmss |
|
@tools:sample/avatars |
Vector drawables that you can use as profile avatars. |
@tools:sample/backgrounds/scenic |
Images that you can use as backgrounds. |
上述表格中不僅有常用文本數據和日期等數據,還提供了一些圖片樣本數據,那么該如何使用呢?很簡單,只需要切換到布局預覽界面,並拖動一個 ImageView 到面板上,然后 Android studio 就會彈出如下界面:
然后選擇 avatars 或者 background/scenic 數據源就可以了。當然你也可以通過 xml 代碼形式來設置:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@android:color/white"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:id="@+id/imageView"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="16dp"
tools:srcCompat="@tools:sample/avatars"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/textView" app:layout_constraintStart_toEndOf="@+id/imageView"
android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="16dp"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random"
tools:maxLines="8"
android:ellipsize="end"
android:textSize="14sp"
android:textColor="@color/title_color" android:layout_marginTop="16dp"
app:layout_constraintHorizontal_bias="0.0"/>
</android.support.constraint.ConstraintLayout>
同樣地,TextView 也可以通過 @tools:sample/lorem/random 來添加樣本數據,如此一來,效果如下:
哈哈,還不錯吧😄。那么問題來了,如果我們想要用自己的樣本數據源呢?
二、自定義 sample data
如果我們想要是用自己定制化的樣例數據,該如何做呢?其實很簡單,只需要在 app 目錄下創建 sample data directory 就可以了:
接下來,我們就可以在里面定制我們自己的數據了,在剛建好的 sampledata 目錄下新建一個 txt 格式的數據文件,如 users,然后在里面創建如下數據:
如此這般,同理創建我們的其他數據: titles、descriptions,然后在上述 card_item_layout.xml 布局文件中替換並使用自己的數據源:
<TextView
android:id="@+id/card_item_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="@+id/card_item_title"
app:layout_constraintEnd_toEndOf="@+id/card_item_title"
app:layout_constraintHorizontal_bias="0.0"
android:textSize="12sp"
android:textColor="#8989ae"
android:layout_marginEnd="16dp"
android:paddingEnd="16dp"
tools:text="@sample/users" />
這里僅以其中一個 TextView 舉例說明,其他同理。
什么?你以為到這里就講完了?哈哈,少年,看你骨骼驚奇,再教你一招來上天入地:通過自定義 Json 格式的 數據來為控件綁定數據:
打完收工👋,還是上面的例子,來看看如何通過 json 數據來綁定:
<TextView
android:id="@+id/card_item_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="@+id/card_item_title"
app:layout_constraintEnd_toEndOf="@+id/card_item_title"
app:layout_constraintHorizontal_bias="0.0"
android:textSize="12sp"
android:textColor="#8989ae"
android:layout_marginEnd="16dp"
android:paddingEnd="16dp"
tools:text="@sample/sample.json/data/username" />
以上操作的時候 Android studio 都會自動提示 sampledata 路徑下的數據文件,Json 格式亦會提示到具體字段。
最后
從 Google Android 官方的進一步動向來看,后續Android更新過程中,布局編輯器將更加強大。從 Tools attributes
到 ConstraintLayout1.1
再到即將到來的 ConstraintLayout2.0
中的 MotionLayout
,我們可以預見:Android 將在 UI 渲染和動畫實現方面進一步解放我們的雙手。后續我將繼續為大家帶來系列文章,敬請期待🌈。