>
* 原文鏈接 : Screenshots Through Automation
* 作者 : Flavien Laurent
* 譯者 : chaossss
* 校訂者: sundroid
* 狀態 : 校訂完成
在發布 App 到利用商店時有1件的事情不能不做,就是上傳最新的高清無碼截圖到利用商店上。可是如果你的 App 有許多頁面,那你每次發布更新都多是1場夢魘,由于你需要1頁1頁地去截圖。為了解決眾多 App 開發者的這個痛點,我將在這篇博文中介紹1個實現自動化截圖的方法:
剛到 Capitaine Train 公司里,就有人讓我造個能自動截圖的輪子,由于我們公司的 App 每次版本更新都讓人很頭疼:問題在于我們的 App 對應有3種裝備,4種語言,也就是有 12 種版本。另外,我們有6個需要截圖的頁面,也就是說,我們每次版本更新都需要72張截圖。我們沒法忍耐這類低效并浪費時間的工作,因而我們經過不懈的努力,找到了1個自動化截圖的方案,在這個方案中,要實現自動化截圖有3個關鍵點:uiautomator 自動化測試, accessibility 和 bash腳本。
uiautomator 是1個用部份封裝代碼將 UI 處理成1個 JUnit 測試用例的框架。這里需要注意的是:被測試的 App 里沒有包括這些測試用例,由于他們在1個獨立的進程中運行。換句話說,你可以把 uiautomator 框架看做1個獨立的機器人,它能幫你在裝備上完成諸如:點擊,轉動,截圖等簡單動作。
豫備知識
在繼續講授之前,我建議你花些時間瀏覽官方文檔,這能幫助你更好地理解接下來的內容。
uiautomator 框架的 API 非常簡單,里面有3個類分別代表了不同類型的 UI 界面元素:
UiObject: 基本界面元素,例如:TextView
UiCollection: 包括多個 UiObject 的界面元素,例如:LinearLayout
UiScrollable: 包括多個 UiObject ,并能轉動的界面元素,例如:ListView
框架里這兩個類你也需要了解:
UiDevice:用于履行裝備常見的動作,例如:點擊按鈕,截圖等等
UiSelector:通過 id, 類型等取得屏幕上的 UI 界面元素
最后,UiAutomatorTestCase 是框架里你絕對不能疏忽的類,由于我們必須通過繼承它來取得1個 uiautomator 測試用例。
固然了,我剛剛提到的這些類在官方文檔里面都有詳細的解釋,另外,文檔還提供了1些示例來幫助我們熟習 uiautomator 。
安裝,創建和運行
接下來我們要做的就是創建 uiautomator ,但很不幸,uiautomator 并沒有1個官方的 Gradle 整合模塊,所以我們必須自己去完成這項工作。把這些工作都完成后,才能在我們的 App 上使用 uiautomator。uiautomator 測試用例的終究輸出應當是1個獨立的 JAR 包。具體步驟以下:
在你的項目里新建1個 Gradle 模塊,并在其中添加與 local.properties 相同的 android.jar 依賴包:
.build.gradle
apply plugin: 'java'
Properties props = new Properties()
props.load(new FileInputStream(file("../local.properties")))
dependencies {
compile fileTree(dir: props['sdk.dir'] + '/platforms/' + androidSdkTarget, include: '*.jar')
}
通過使用 local.properties 和 gradle.properties 新建1個 ant 文件,使其取得與項目相同的配置信息(target, sdk path):
build.xml
<?xml version="1.0" encoding="UTF⑻"?>
<project name="uiautomator" default="help">
<loadproperties srcFile="../local.properties" />
<loadproperties srcFile="gradle.properties" />
<property name="target" value="${androidSdkTarget}" />
<import file="${sdk.dir}/tools/ant/uibuild.xml" />
</project>
使用ant 構建JAR(不要使用Gradle構建),并把它加到你的裝備中,然后運行你的測試用例。
$ ant build
$ adb push uiautomator.jar data/local/tmp
$ adb shell uiautomator runtest uiautomator.jar -c com.your.TestCase
自動切換設置信息
現在我準備講授怎樣在設置中自動切換設置項和設置信息(特別是從1個語言切換到另外一個語言)。首先,這是1個練習使用 uiautomator 的機會。同時,這也是自動化截圖的關鍵步驟。但你要記住,我接下來介紹的只是1個能在 Android 5.0 系統上正常使用的辦法,如果你有更好的建議或想法,也能夠通過留言和我交換,1起優化這個步驟。
mUiDevice.openQuickSettings();
new UiObject(new UiSelector().resourceId("com.android.systemui:id/settings_button")).click();
UiScrollable scrollable = new UiScrollable(new UiSelector().resourceId("com.android.settings:id/dashboard"));
scrollable.getChildByText(new UiSelector().className(FrameLayout.class), "Language & input", true).click();
UiScrollable scrollable = new UiScrollable(new UiSelector().className(ListView.class));
scrollable.getChildByText(new UiSelector().className(LinearLayout.class), "Language", true).click();
UiScrollable scrollable = new UiScrollable(new UiSelector().className(ListView.class));
scrollable.getChildByText(new UiSelector().className(LinearLayout.class), "Fran?ais (France)", true).click();
Locale.setDefault(new Locale("fr"));
完成了上面的操作后,你還需要強迫設置新的語言環境以免 uiautomator 操作進程中保存了翻譯緩存。
小提示
為了保證 uiautomator 的穩定性,當你在使用 uiautomator 時,必須關掉裝備上的所有動畫效果(你可以通過下面的設置完成:Settings > Developer options > Window animation|Transition animation|Animator duration scale)
如果你想打 Log 方便你的調試,你可使用 android.util.Log。為了更好地辨別 Log 信息,你可使用特定的標記來挑選它們。
每次你需要在 View 的不同層級間切換都要使用 uiautomatorviewer。由于它能為你提供1個精確的選擇器,使你能夠取得目標 UI 界面元素(uiautomatorviewer 在 sdk/tools/uiautomatorviewer 里)。
記住,uiautomator 測試用例不是 Android 的測試用例,所以你不需要使用任何情勢的 Context。
你不能通過 uiautomator 進入你的 App 類,你只能援用 Android 框架中的類。
你可以在命令行中使用 -e 命令把 uiautomator 命令行的參數傳遞到測試用例類中,又或是使用測試用例類中的 UiAutomatorTestCase.html#getParams()。
這樣處理下來,你會發現自動完成語言的切換很簡單對吧?uiautomator 雖然是個很好的工具,但如果你的 App 不是可訪問的,它就沒甚么用了。特別是你的 App 需要創建完全自定義的 View 時,便可能會出現各種問題,所以接下來我們要解決的問題就是讓 App 可以被訪問,特別是自定義 View。
可訪問性對1個 App 來講非常重要,其作用主要體現在兩個方面:有些用戶/開發者需要它(但總有開發者會疏忽這個需求),另外,uiautomator 都以可訪問性為基礎,也就是說,如果1個利用不能提供可訪問的入口,我們將沒法在其中使用 uiautomator 自動化測試工具。
大部份情況下,你都沒有必要讓你的 App 可以被其他利用訪問。但事實上,大部份 View 都是可訪問的,例如 TextView,ListView 等等。不過在你使用自定義 View 時,取得訪問性可能會麻煩點,由于這需要你花費1些工夫去改變其中的代碼。
在 Capitaine Train App 里,為了滿足對日歷視圖的特殊需求,我們創建了1個自定義 View。這個 View 是基于 ListView 設計的,ListView 中的每項都有好幾個自定義 View,并且每個自定義 View 都代表1個月(我們稱為 MonthView)。MonthView 是1個純潔的 View,它繼承于 View,并沒有子類。這樣使得 MonthView 中的1切都需要通過 onDraw() 方法進行繪制。因此,MonthView 在默許情況下不能被訪問。
首先要做的事情很簡單:使用 View#setContentDescription 方法為每個 MonthView 設置內容描寫,這樣我們能夠把 ListView 轉動到1個特殊的月份上。
然后,1旦 ListView 停留在某1個給定的月份上,我們希望我們能夠選擇1個肯定的日期。為了實現這個需求,我們需要使 MonthView 的內容是可訪問的。榮幸的是,Android 的支持庫在類似的處理上提供了1個很有用的 Helper類:ExploreByTouchHelper。由于 MonthView 不是以樹形結構結合展現其中的 View 集合,所以創建偽樹狀結構的 View 集合需要基于觸摸反饋實現。
為自定義 View 實現 ExploreByTouchHelper
我們有4個方法可以實現:
getVirtualViewAt(float x, float y)
返回參數 x,y地方對應的虛擬 View 的 id。如果對應位置上沒有虛擬 View,則返回 ExploreByTouchHelper.INVALID_ID
getVisibleVirtualViews(List virtualViewIds)
將自定義 View 中所有虛擬 View 的 id 添加到 virtualViewIds 數組中。
onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event)
讓虛擬 View 的相干信息可以被訪問,例如:文字,內容描寫
onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node)
讓給定結點能夠訪問虛擬 View 的相干信息,例如文字,內容描寫,類名,與父類的關系。如果二者之間產生了交互,你必須在給定結點中說明。
onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments)
在虛擬 View 中實現某種動作(在前面的方法中被指定)
怎樣讓 ExploreByTouchHelper 的接口變得更簡單:
YourAccessibilityTouchHelper.java
private class YourAccessibilityTouchHelper extends ExploreByTouchHelper {
public YourAccessibilityTouchHelper(View forView) {
super(forView);
}
@Override
protected int getVirtualViewAt(float x, float y) {
final VirtualView vw = findVirtualViewByPosition(x, y);
if (vw == null) {
return ExploreByTouchHelper.INVALID_ID;
}
return vw.id;
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
for (int i = 0; i < mVirtualViews.size(); i++) {
mVirtualViews.add(mVirtualViews.get(i).id);
}
}
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
final VirtualDayView vw = findVirtualViewById(virtualViewId);
if (vw == null) {
return;
}
event.getText().add(vw.description);
}
@Override
protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {
final VirtualDayView vw = findVirtualViewById(virtualViewId);
if (vw == null) {
return;
}
node.setText(Integer.toString(vw.text));
node.setContentDescription(vw.description);
node.setClassName(vw.className);
node.setBoundsInParent(vw.boundsInParent);
}
}
在你的自定義 View 中使用 Helper 類
我們需要在 ListView.getView 方法被履行后通過 setAccessibilityDelegate() 方法重設代理,由于我們需要實現 dispatchHoverEvent() 方法來激活對觸摸事件的探索。(如果你的自定義 View 沒有在 ListView 中被使用的話,只需要在構造器中設置代理)。
YourCustomView.java
public class YourCustomView extends View {
private final YourAccessibilityTouchHelper mTouchHelper;
public YourCustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTouchHelper = new YourAccessibilityTouchHelper(this);
}
private void setAccessibilityDelegate() {
setAccessibilityDelegate(mTouchHelper);
}
[...]
public boolean dispatchHoverEvent(MotionEvent event) {
if (mTouchHelper.dispatchHoverEvent(event)) {
return true;
}
return super.dispatchHoverEvent(event);
}
用 uiautmatorviewer 檢查你的接口能否正常運行
如果1切都正常運行,在你用 uiautmatorviewer 截圖后,你應當能在虛擬 View 圖層看到在可訪問結點中預設置的所有信息。
另外一方面,在我寫這篇博文的時候我發現 Capitaine Train App里的1個問題:每個虛擬 View 的類名都是 com.capitainetrain.x,由于我們忘了用 Proguard。
現在 App 中的1切都是可訪問的,我們總算可以在 App 中順利使用 uiautomator 進行自動化截圖了。打鐵趁熱,我們無妨對我們的代碼稍作修改,讓它能夠“優雅地截圖”。
這篇博文要講授的最后1個問題就是怎樣改進 uiautomator ,使得它能在多種語言中優雅地自動截圖。實現這個功能需要兩個步驟:第1,使用 bash 腳本運行 uiautomator 測試用例,并依照你需要的圖片數量進行自動化截圖,以后用 imagemagick 處理你取得的照片。
首先要做的就是創建 uiautomator JAR包,然后運行測試用例。由于你已在前面的講授中學習了怎樣在測試用例中轉換語言,所以你只需要傳遞兩個參數到測試用例中:當前設置中使用的語言和你將要切換的語言。
screenshot.sh
# Build and push the uiautomator JAR
ant build
adb push bin/uiautomator.jar data/local/tmp
adb shell uiautomator runtest uiautomator.jar
-e current_language ${currentLanguage}
-e new_language ${newLanguage}
-c com.your.TestCase
接下來我們只要再創建1個能夠切換語言,打開 App并截圖的簡單測試用例就能夠啦:
TestCase.java
public class TestCase extends UiAutomatorTestCase {
[...]
@Override
protected void setUp() throws Exception {
super.setUp();
final Bundle params = getParams();
mCurrentLanguage = params.getString("current_language");
mNewLanguage = params.getString("new_language");
}
public void test() throws Exception {
switchLanguage(mCurrentLanguage, mNewLanguage);
openApp();
takeScreenshot("data/local/tmp/screenshots");
}
}
現在截圖都被貯存在裝備里了,我們只需要把它們取出來就大功告成了:
screenshot.sh
mkdir screenshots
adb pull data/local/tmp/screenshots screenshots
在多語言環境中運行測試用例。它會從裝備當前使用的語言開始運行,由于我找不到1個適合的方式去表示它,然后會在不同的語言環境下(我們需要截圖的那些語言)運行測試用例。
screenshot.sh
screenshot() {
currentLanguage=$1
newLanguage=$2
adb shell uiautomator runtest uiautomator.jar
-e current_language ${currentLanguage}
-e new_language ${newLanguage}
-c com.your.TestCase
}
screenshot $deviceLanguage fr
screenshot fr en
screenshot en de
App 每次卸載/安裝后在相同的環境下運行測試用例都能正常地實現自動化截圖的功能:
screenshot.sh
screenshot() {
currentLanguage=$1
newLanguage=$2
# Uninstall/Install the app
adb uninstall com.your.app
adb install ../app/build/outputs/apk/yourapp-release.apk
adb shell uiautomator runtest uiautomator.jar
-e current_language ${currentLanguage}
-e new_language ${newLanguage}
-c com.your.TestCase
}
最后把所有模塊糅合在1起:
screenshot.sh
screenshot() {
currentLanguage=$1
newLanguage=$2
# Uninstall/Install the app
adb uninstall com.your.app
adb install ../app/build/outputs/apk/yourapp-release.apk
# Run the test case
adb shell uiautomator runtest uiautomator.jar
-e current_language ${currentLanguage}
-e new_language ${newLanguage}
-c com.your.TestCase
mkdir screenshots
adb pull data/local/tmp/screenshots screenshots
}
# Build and push the uiautomator JAR
ant build
adb push bin/uiautomator.jar data/local/tmp
# Build the APK
cd .. && ./gradlew assembleRelease && cd uiautomator
# Screenshot everything
screenshot $currentLanguage fr
screenshot fr en
screenshot en de
美化截圖
分享1篇好文:Creating professional looking screenshots。
每個 App 的運營者都應當盡其所能美化 App 的截圖,由于這是用戶在利用商店中對 App 的第1印象。大多數情況下,用戶都不會瀏覽利用的描寫,而是直接打開利用的截圖,由于瀏覽文字比看圖片更費力。雖然不能說經過下面的處理能取得完善無瑕的圖片,但也在水平線以上了。那末甚么樣的 App 截圖是優雅的截圖呢?
始終保持狀態欄的整潔
移除導航欄
適配多種屏幕的尺寸
第2點可以用1個超奇異的工具―imagemagick 實現,雖然它的官方文檔非常大,但我們用不到那末多的特性,所以我們只需要關注兩個特性:組合和轉換。
用組合圖覆蓋狀態欄
組合圖是用來把1個圖片覆蓋到另外一個上面的,這是取得簡潔狀態欄的完善辦法。
composite -quality 100 -compose atop clean_status_bar.png screenshot.png clean_screenshot.png
通過轉換裁剪導航欄
轉換特性被用于轉換圖片的格式,使其格式與裁剪后的圖片相同,這是從截圖中移除導航欄的完善辦法。
convert -quality 100 screenshot.png -gravity South -chop 0x144 clean_screenshot.png
144是在Nexu5上導航欄的高度像素值。
結論
由于有了這篇博文,通常要花費半天,乃至1天的截圖工作現在能通過 Capitaine Train 上用的這個自動化截圖工具縮短到 20~30 分鐘完成(我相信沒有人想手動地做這些工作,或由于厭棄這樣的工作,從不更新 App 的截圖)。這個工具能高效地節省時間,如果能夠更多的人和資源投入到這個工具的開發當中,我相信這個工具還能變得更好,也不會那末容易出錯和崩潰。
接下來可能做的:
使用 Google Play 發布的 API 簡化上傳這些自動生成的截圖的流程,并把這個工具整合到 Jenkins 里,讓 App 每次版本更新都能自動地獲得最新的截圖,并將其顯示在利用商店中。