鄭重聲明:
html
- 一、筆者只是出於對技術的好奇,無惡意破壞APP;
- 二、僅用於技術學習,尊重原開發者的勞動成果,未用於商業用途;
記得開完「所謂的需求評審」後的第三天,設計師丟來了一紙設計稿,有個這樣的頁面:android
而後過來和我嘰裏呱啦地說了一堆:nginx
這個頁面顯示全部課程,而後能夠滑動,滑動的同時背景也要跟着動…git
聽得我是:???github
那句 短小但精悍 的口頭禪脫口而出:web
直接把你借(chao)鑑(xi)的競品APP拿來~shell
接着設計師打開競品APP「XX英語」並給我展現了一番:瀏覽器
Yo~ 遊戲通關類的學習APP耶,記得很久之前在一款英語APP上也看到這種頁面,不過人家用Cocos2d作的,若是這個也是這樣,就無法作了,先來辨別「頁面是否是原生寫的」。性能優化
辨別方法很簡單,手機依次:bash
打開「開發者工具」 -> 勾選「顯示佈局邊界」
若是出現以下所示的邊框和線:
則說明就是原生寫的,不然就多是Cocos2d,網頁或者自定義控件等了。既然原生,說明有戲,不過可能要花些時間,習慣性地「裝出一副很爲難的樣子」
套用「應該、也許、可能」等不肯定的辭藻勸退設計師後,開始把玩起了這款競品APP,第一感受「精美」,屌打我方APP,「設計,動效,原畫,內容」全方位碾壓,不知道我方產品弟弟哪來的自信想着撈錢:
原本只是想看下這個頁面是怎麼實現的,結果卻「一發不可收拾」:
那種感受就好像:
- 有個認識了好久的 老司機,說要帶你去一趟 大保健,漲漲見識;
- 你呢:早就據說過大保健了,但沒人帶路不敢去,有點忐忑,慫,還嫌有點髒;
- 礙於面子,你仍是接受了邀請,點了個最便宜的 洗腳,心想就洗個腳,洗完就走;
- 經理 老練地把你帶到一個 有些陰暗 的房間,讓你等候,技師立刻就來;
- 經理走後,你 好奇得像個孩子,翻遍了整個房間,卻沒找到洗腳用的盤子;
- 短促的敲門聲想起,「先生,能夠進來嗎?」,甜美的聲音 嚇得你趕緊坐回原位;
- 「進來吧」,一位 身材姣好的女子 推門而入,「靚仔,久等了,很差意思,今晚人太多了」;
- 昏暗的燈光 和 技師臉上濃濃的妝,讓你有些看不清她的模樣,直覺告訴你她可能芳齡25-35之間;
- 你也很差一直盯着技師的臉,畢竟這樣不禮貌,一時間不知說啥,氣氛略顯尷尬;
- 你憋出了一句:「那個,我不是點了洗腳嘛,怎麼沒見到洗腳盤?」
- 技師 微微一笑:「噢,洗腳的技師都上鍾了,估計要等2個小時」,並再次強調今晚人多;
- 你有些 不滿:「那怎麼辦,我錢都給了,技師不夠,經理也沒和我說啊!」;
- 技師 略帶歉意:「靚仔,真的很差意思,要不給你換成 推背?」
- 你:「推背?價錢同樣嗎?幹嗎的?」
- 技師:「就是推推背,按摩按摩穴位,促進血液循環,就加100塊錢。」
- 你:「哇,貴這麼多,我洗個腳才45,算了算了,不按了」,而後準備穿鞋子走人;
- 技師挽住你的手臂:「靚仔,你朋友點了這個,你出去等也要等45分鐘,可貴來一次,試試嘛!」;
- 你轉念一想:也對,出去等無聊不說,老司機出來看到我坐着,多沒面子啊。
- 貴100就100吧,反正就來一次(然而這東西和女裝同樣,只有零次和無限次)
- 「行吧,加100推拿」,技師一聽,不由 笑靨如花,你竟看得有些走神;
- 有些靦腆地和技師聊着天,過了一下子,經理敲門,送進來了一個小籃子;
- 你瞄了瞄籃子裏裝的東西:幾個小罐,像蚊賬同樣通透的布,以及 兩顆果凍;
- 布我能夠理解,多是拿來擦拭的,這兩個果凍是?零食麼?可是未免太摳門了吧?
- 技師一聲:「靚仔,牛奶仍是精油開背」,把你的發散思緒拉了回來;
- 「牛奶吧」,按技師吩咐,褪去上衣,一趴,接着開始推背,手勢真的不錯,
- 按得你是一陣酥軟,加之技師的對你的一頓吹捧,不由有些飄飄然;;
- 45分鐘眨眼就到,門口的上鍾鈴響起宣告了這次推拿的結束;
- 你有些 意猶未盡,技師彷彿看穿了你的心思,「靚仔,舒服吧,要不要 加鍾?」
- 你:「嗯,挺舒服的,加鐘的話多少錢,仍是推背嘛?」
- 技師忽而 臉泛微紅,「也是100,仍是推,就是推的方式和部位有點不同…」
- 你彷佛get√到了什麼,「Yo?有點意思,行,加100,我倒要看你怎麼推。」
- 技師:「嗯」,說完拿出小籃子裏的 那塊布 和 兩顆果凍;
- 此刻你終於知道了:
- 那 不是一塊布,而是一件 很是通透的衣服;
- 而 兩顆果凍 也不是零食,而是「水晶之戀」的道具;
- …一頓翻雲覆雨的馬賽克,To be continue…
以上故事純屬虛構!!!筆者也是從別人那裏聽回來的,沒去過這種地方!!!
只是想表達「扒代碼」是一件頗有趣的事,從想扒「一個UI效果」到扒「全部UI效果」,再到扒「數據」和「架構」,扒得一點不剩,最後再「爲我所用」的過程。像極了從一開始只是想「洗腳」到後面的「水晶之戀」「環遊」「冰火兩重天」等的你。不過仍是建議多看看「優秀的開源項目」,畢竟「路邊野花」(偷代碼),吸引你的不是香,而是野。筆者沒啥文化,只能找到這種粗俗的例子來表達本身的感覺,還望讀者 海涵 ~
行吧,廢話說得有點多了,繼續本節內容!
對了,過後從老司機那裏得知:這裏 並無洗腳的技師…
從開發者助手得知了一些有用信息:
- 一、應用包名:com.knowbox.en
- 二、當前頁面名稱爲:MainActivity
- 三、當前Fragment爲:MapFragment
接着鍵入下述adb命令,獲取當前棧頂Activity相關的信息:
adb shell dumpsys activity top > info.txt
複製代碼
打開info.txt輸出文件,定位到MainActivity,看下佈局層次結構:
BaseUIRootLayout,MapViewPager,五個RecyclerView映入眼簾,em…實現原理該不會是:「滑動偏移錯位」
即:當一個列表滑動時,其餘列表跟着滑動不一樣的距離,好比列表滑動10,其餘列表分別滑動102,103, 10*4
猜測有了,接下來反編譯驗證一波,沒加固,直接執行反編譯批處理腳本(本身寫的):
靜待反編譯完成:
接着,Android Studio導入反編譯後的jadx目錄(apktool目錄是smail代碼的):
接着全局搜索文件:MapFragment,而後文件內搜:R.layout.,找到佈局文件名:
接着全局搜佈局文件:layout_main_map
em…佈局和咱們adb dumpsys的內容同樣,五個RecyclerView,接着打開MapFragment開始跟代碼,
然而開頭OnScrollListener的就給出了答案:
這裏的bcde是混淆變量名,往下翻能夠看到:
2131690465是控件ID,全局搜下,在R文件中能夠找到對應值
找到對應的id,這裏直接替換:
見名知意,前中後三個背景圖和一個線,剩下一個應該就是設置了這個滾動監聽的列表了,定位下:
行吧,就是滑動偏移錯位,噢,忽然想到一個問題,幾個列表都能滑動耶,怎麼以這個列表爲準:
onTouch()返回true,使得Recyclerview的onTouchEvent方法不被調用(從而屏蔽用戶滑動與點擊)。
行吧,大概瞭解了,開始搬運~
無腦搬運佈局,只是外層用的ConstraintLayout佈局包裹:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_back_level_bg"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_middle_level_bg"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_front_level_bg"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_line"
android:layout_width="0dp"
android:layout_height="80dp"
android:paddingStart="135dp"
android:paddingEnd="80dp"
app:layout_constraintStart_toStartOf="@id/rv_main_homework"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:clipToPadding="false"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_main_homework"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="55dp"
android:paddingEnd="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:clipToPadding="false"/>
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼
Tips
這裏有兩個RecyclerView用到了 android:clipToPadding="false",做用是讓佈局能繪製到padding區域,不是很明白,看下分別設置true和false的效果就知道了:
![]()
![]()
另外,須要和另一個屬性: clipChildren 進行區分,這個屬性是: 設置子view是否能夠超出父view!!!
三個背景圖用一個Adapter,在 res和assets目錄 中並無找到對應的圖片文件,估摸着素材是聯網下載的,猛地想起,一開始進入APP的時候有過下載資源。清理下數據,打開Fiddler抓下包,打開APP:
20多M耶,也沒加什麼校驗,瀏覽器直接打開,把文件下載到本地解壓:
em…看下文件名,不難發現有三類圖片,前中後,依次打開圖片:
圖片高度都是750,除了最後一張寬度是不肯定的,其餘都是500,這裏就不去下載解壓了,直接把圖片都丟drawable-xxhdpi文件夾中,可是有一點要注意「圖片名不能數字開頭!!!」,否則等下索引會報錯,開頭所有加上bg_前綴吧,懶得一個個手動改了,隨手寫個批量重命名腳本吧:
import os
pic_source_dir = os.path.join(os.getcwd(), "lisk5"+os.sep) # 原圖路徑
if __name__ == '__main__':
file_list = []
f = os.listdir(pic_source_dir)
for i in f:
if i.endswith(".png"):
os.rename(os.path.join(pic_source_dir, i),
os.path.join(pic_source_dir, "bg_%s" % i))
print("批處理完成!")
複製代碼
在寫Adapter前,先來寫每一個Item的佈局吧,無腦 佈局套ImageView,高度佔滿,寬度自適應,示例以下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:src="@drawable/bg_1_back_01" />
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼
不過,學過性能優化的都知道:「應儘可能減小沒必要要的佈局層次嵌套」,咱們這樣玩的話,要疊三層ConstraintLayout… 其實吧,動態添加一個ImageView就行了,只是要 肯定(計算) 好它具體的寬高。嘖嘖,看下APP是如何實現的,搜文件 MainHomeworkBgAdapter,定位到 setLayoutParams:
哇,這裏好多a啊,一個個來看,先是 ViewHolder的a:
噢,這是定義了一個ImageView,接着到 onBindViewHolder 處的兩個 a(((xxx) this.c.xxx:
噢,執行a函數,做用:利用Bitmap獲取圖片寬度返回,同時ImageView設置圖片。
接着到this.a,即最外層的a,存儲寬度的臨時變量,這一段代碼有點意思:
咱們從解壓的資源包知道,除了最後一項外,其餘圖片寬度皆爲500,而服務器內部錯誤碼也是500~
這個臨時變量在構造方法中完成了初始化:
定位到UIUtils 的 b函數:
em,就是獲取屏幕的高度,到此整個流程就一清二楚了,動手寫出Adapter
接着到虛線列表,打開res和assets沒發現虛線圖片,應該就是自定義View了,回到 MapFragment.kt,定位到設置adapter的位置,能夠看到這個LineAdapter:
進 LineAdapter,能夠看到ViewHolder裏有一個LineView,跟進去:
進 LineView,代碼以下:
簡單說下流程:
- 一、構造方法:setWillNotDraw(false),沒記錯的話,重寫ViewGroup才須要用到,設置false讓ViewGroup能夠onDraw(),裏面調用了一個方法a;
- 二、方法a:初始化Paint畫筆和Path路徑;
- 三、onLayout方法:獲取寬高;
- 四、onDraw方法:根據向上仍是向下設置起始和終點Y座標,接着繪製直線
- 五、setIsUp方法:設置繪製的方向是向上仍是向下。
一樣搬運一波代碼:
接着回到LinearAdapter,比較簡單,核心的就這裏:
先是SetVisibility這裏,0和4分別是「VISIBLE」和「INVISIBLE」,接着是圈住的判斷條件:頭尾虛線不顯示能夠理解,就是這個this.a 是幹嗎的?直接搬運代碼,看下不判斷會怎樣:
運行後:
臥槽,少了一個,因此這個this.a究竟是幹嗎的?能夠看到構造方法中傳入了一個z,跟:
z的初始值爲false,判斷了一波this.j.i是否等於1,是的話等於true,那麼this.j.i究竟是啥?這裏就不跟了,直接用「smail動態調試」這個APP,「前戲如何準備,下一節教你」,這裏假設前戲已作好,開始調教~ 找到大概的位置下斷點:
終端命令執行腳本:
手機顯示Waiting for Debugger,等待 插入…呸,調試,選擇APP進行,點擊OK
來到斷點位置,程序會自動掛起,AS彈出Android Debugger窗口。
能夠看到傳入Adapter的參數50和true,而後是這個this.j.i,可是確是一個字符串:「1-49」,臥槽,判斷字符串是否等於整數??? 什麼鬼?
if(字符串 == 1)
複製代碼
編譯都不經過吧,大哥,直接看 this.j:
定位到OnlineMainCourseIndexInfo類:
從parse那裏能夠看出這個i應該是當前地圖的ID,可是卻變成了「1-49」,這個更像j當前地圖等級吧,而g更像是openCartoonVideo,這裏應該懂了吧,不是一一對應的!因此其實對應的參數是h,即1,表明第幾關,那直接忽略吧,修改後的代碼以下:
運行後:
能夠,就是咱們想要的效果,剩下前面的Adapter了。
直接定位到MainHomeworkAdapter:
嘖嘖,RecyclerView多Item佈局,見名知意,表頭表尾,以及中間,搬運寫出Adapter雛形(這裏就不寫點擊事件了)
數據類有兩個變量暫且不知道是幹嗎的:
無腦搬運三個佈局,接着開始寫Adapter,先是CommonAdapter,部分代碼以下:
而StartHolder和EndHolder則比較簡單:
Adapter寫好了,接着就是造數據了,依舊下斷點調試,
複製粘貼,循環造點假數據:
修修補補後,運行下看下效果:
行吧,算是偷取完成了~
咱們都知道能夠調用TextView的setTypeface設置字體,若是一個APP用到了多個字體包,每次都去設置顯得有些繁瑣,這個APP直接重寫TextView,直接XML引用,方便多了,筆者在原先基礎上作點小改動,有默認字體,可在XML中單獨設置字體,attrs.xml中添加屬性一枚:
接着EnTextView繼承TextView,獲取屬性,設置字體:
接着XML中設置下屬性便可:
其實競品中大部分看起來很精美的動畫都是用到了Aribnb的Lottie庫,好比下面這個動畫(漂浮的大象,還會眨眼):
還有白圈擴散波紋的動畫,若是讓你來作,你會怎麼作?
- 一、幀動畫:須要添加大量圖片(尺寸適配),勢必會致使APK體積暴漲;
- 二、Gif:Gif圖佔用空間較大,且需適配多種屏幕,影響同上;
- 三、屬性動畫 + 圖片 + SVG:繁瑣且不易維護,稍做修改可能就要推倒重來;
用Lottie庫可讓咱們開發仔免於糾結複雜的動畫效果,網上關於它的介紹有不少,這裏就再也不作復讀機了,直接說怎麼玩,須要:
Step 1:設計師經過AE(After Effects)和 Bodymovin插件 將動畫導出JSON文件;
Step 2:開發仔把JSON文件丟到app/src/main/assets目錄下
Step 3:build.gradle導入lottie-android庫,XML中引入LottieAnimationView直接使用。
更多使用說明可見:
搬運:
接着補全下右側顯示動畫,點擊後滾動會起始位置
運行效果以下:
雖然前面立FLAG說要「扒全部的UI效果」,但卻只演示一個,畢竟寫文章的目的只是展現技法,讓讀者觸類旁通,並且扒別人源碼也不是件簡單的事情。一堆混淆的abcd看到眼花,而後各類繼承父類嵌套,耦合,一堆沒用到的代碼,要把一個單獨的控件抽取出來,很是耗費時間。仍是那句話,設計或產品讓抄的時候再去扒,會實際一些,帶着目的去看源碼!UI相關的就先到這裏,下節講解一波,筆者扒別人APP用到的全部「基礎逆向操做」謝謝~
對了,混掘金也挺久了,繼白嫖筆記本後,前些天又白嫖了一個鼠標墊,感恩!
意思意思送「一本本身寫的Python爬蟲入門書」吧,評論區留言抽,包郵,下週五抽~
參考文獻: