撩妹神器,打造圣誕雪花特效附加線上熱修復(fù)
相信去年圣誕節(jié)打開(kāi)過(guò)手機(jī)淘寶的童鞋都會(huì)對(duì)當(dāng)時(shí)的特效記憶猶新吧:全屏飄雪,旁邊還有個(gè)小雪人來(lái)控制八音盒背景音樂(lè)的播放,讓人有種身臨其境的感覺(jué),甚至忍不住想狠狠購(gòu)物了呢(誤),大概就是下面這個(gè)樣子滴:
嗯,確實(shí)很炫,那么我們一步步去分析是如何實(shí)現(xiàn)的:
一、實(shí)現(xiàn)下雪的 View
首先,最上面一層的全屏雪花極有可能是一個(gè)頂層的View,而這個(gè)View是通過(guò)動(dòng)態(tài)加載去控制顯示的(不更新淘寶也能看到這個(gè)效果)。那么我們先得實(shí)現(xiàn)雪花效果的 View,人生苦短,拿來(lái)就用。打開(kāi) gank.io,搜索"雪花":
看樣子第7個(gè)庫(kù)就是我們想要的了,點(diǎn)進(jìn)源碼,直接 download 不解釋?zhuān)浀?star 一個(gè)支持作者。那么現(xiàn)在我們的項(xiàng)目中就有一個(gè)完整的下雪效果 View 了。
二、實(shí)現(xiàn)雪人播放器 View
這個(gè)一張雪人圖片+一個(gè)按鈕即可實(shí)現(xiàn),就不多解釋了。接下來(lái)需要一段圣誕節(jié)音頻,直接進(jìn)行在線音頻播放無(wú)疑是節(jié)省空間的好方案?!何业幕逍缓嫱谐龅募拍鹈鄣姆諊鸁o(wú)疑是最適合圣誕節(jié)的,因此我們得到了『神曲』URL 一枚:
http://cdn.ifancc.com/TomaToDo/bgms/my_hbx.mp3
接下來(lái)要找一個(gè)小雪人的圖片當(dāng)作播放器的背景,那么阿姆斯特朗...不對(duì),是這個(gè):
嗯,相當(dāng)可愛(ài)喜慶。那么播放器核心代碼如下:
- package com.kot32.christmasview.player;
- import android.content.Context;
- import android.media.AudioManager;
- import android.media.MediaPlayer;
- import android.util.AttributeSet;
- import android.view.View;
- import android.widget.Toast;
- import com.kot32.christmasview.R;
- import java.io.IOException;
- /**
- * Created by kot32 on 16/12/8.
- */
- public class MyPlayer extends View {
- public MediaPlayer mediaPlayer;
- public MyPlayer(Context context) {
- super(context);
- init();
- }
- public MyPlayer(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
- private void init() {
- setBackgroundResource(R.drawable.pig);
- mediaPlayer = new MediaPlayer();
- mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
- playUrl("http://172.20.248.106/IXC5b415fcacfc3c439e25a3e74533d2239/TomaToDo/bgms/my_hbx.mp3");
- Toast.makeText(getContext(), "開(kāi)始播放", Toast.LENGTH_SHORT).show();
- setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (!mediaPlayer.isPlaying()) {
- mediaPlayer.start();
- Toast.makeText(getContext(), "繼續(xù)播放", Toast.LENGTH_SHORT).show();
- } else {
- mediaPlayer.pause();
- Toast.makeText(getContext(), "暫停播放", Toast.LENGTH_SHORT).show();
- }
- }
- });
- }
- public void playUrl(String videoUrl) {
- try {
- mediaPlayer.reset();
- mediaPlayer.setDataSource(videoUrl);
- mediaPlayer.prepare();//prepare之后自動(dòng)播放
- mediaPlayer.start();
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- try {
- media
三、動(dòng)態(tài)加載思路
上面基本實(shí)現(xiàn)了在本地的雪花以及播放音樂(lè)效果,那么在不更新主程序的情況下,如何將這兩個(gè)View動(dòng)態(tài)加載到主程序當(dāng)中去呢?
首先我們明白,Android 的DexClassloader 是擁有加載任意APK 中任意類(lèi)的能力的,只是有以下限制:
- 加載出的Activity 由于不在宿主 Manifest 文件中聲明,因此框架無(wú)法找到并初始化這個(gè)Activity。
- 加載出的Activity 不具備生命周期,理由同上。
- 加載出的類(lèi)的Resource 文件id 會(huì)和主程序混淆在一起。
由于我們只是加載View,并不是加載整個(gè)Activity,所以前兩個(gè)問(wèn)題并不會(huì)遇到,而第三個(gè)問(wèn)題可以想辦法解決掉。
在主程序中我們也要做這三件事:
- 把能夠裝載View的ViewGroup 的空位留出來(lái)
- 去獲取更新的patch包
- 把View 從apk包中加載出來(lái)之后,放進(jìn)留好的ViewGroup 中。這樣一來(lái),不僅是圣誕節(jié),在之后的各種活動(dòng)上都可以在線去加載活動(dòng)的View。
四、開(kāi)始加載
在加載View 之前,首先要意識(shí)到這個(gè)View 是引用了圖片資源的(小豬圖片),因此我們要解決資源問(wèn)題:
- private void initResource() {
- Resources resources = getContext().getResources();
- try {
- AssetManager newManager = AssetManager.class.newInstance();
- Method addAssetPath = newManager.getClass().getMethod("addAssetPath", String.class);
- addAssetPath.invoke(newManager, DynamicViewManager.getInstance().getUpdateFileFullPath());
- Resources newResources = new Resources(newManager,
- resources.getDisplayMetrics(), resources.getConfiguration());
- Reflect.onObject(getContext()).set("mResources", newResources);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
上面代碼的作用是:把添加了外部更新包路徑的資源管理器賦值給了App原來(lái)的資源管理器,也就是說(shuō)現(xiàn)在可以在宿主中訪問(wèn)插件資源了。
核心加載代碼如下:
- DexClassLoader classLoader = new DexClassLoader(apkFile.getAbsolutePath()
- , "dex_out_put_dir"
- , null
- , getClass().getClassLoader());
- Class newViewClazz = classLoader.loadClass("view's package name");
- Constructor con = newViewClazz.getConstructor(Context.class);
- //first use Activity's Resource lie to View
- if (dynamicView == null) {
- dynamicView = (View) con.newInstance(getContext());
- }
- //Replace the View's mResources and recovery the Activity's avoid disorder of Resources
- Reflect.onObject(getContext()).set("mResources", null);
- getContext().getResources();
- RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(DisplayUtil.dip2px(getContext(), viewInfo.layoutParams.width),
- DisplayUtil.dip2px(getContext(), viewInfo.layoutParams.height));
- layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
- addView(dynamicView, layoutParams);
中間對(duì) mResources 的操作的作用是:將宿主的Activity 的mResources 重置,避免在Activity 中使用資源時(shí)和插件沖突。
然而機(jī)智的我已經(jīng)把更新包下載、版本管理、動(dòng)態(tài)加載都封裝好了,所以正確的加載方式是:
引用它:https://github.com/kot32go/dynamic-load-view
然后:
1.宿主聲明:
- <RelativeLayout
- 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:background="@drawable/tb_bg"
- >
- <com.kot32.dynamicloadviewlibrary.core.DynamicViewGroup
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:uuid="activity_frame">
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="原始頁(yè)面"
- />
- </com.kot32.dynamicloadviewlibrary.core.DynamicViewGroup>
- <com.kot32.dynamicloadviewlibrary.core.DynamicViewGroup
- android:layout_width="60dp"
- android:layout_height="60dp"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- app:uuid="activity_player">
- </com.kot32.dynamicloadviewlibrary.core.DynamicViewGroup>
- </RelativeLayout>
以上聲明了主界面的布局,當(dāng)然,在動(dòng)態(tài)加載之前除了原有的"原始頁(yè)面"TextView,是不會(huì)有任何其他東西的,也就是圣誕節(jié)來(lái)臨之前的程序。注意:uuid 會(huì)和在線包相匹配。
2.打插件包
其實(shí)就是把之前包含了我們所寫(xiě)的兩個(gè)View(雪花和雪人)的程序打包成apk??梢圆缓灻?。
3.把插件包放到服務(wù)器
在服務(wù)器返回的JSON中聲明插件包地址和動(dòng)態(tài)View 的一些參數(shù),這里的演示程序請(qǐng)求地址為:
http://tomatodo.ifancc.com/php/dynamicView.php
返回值為:
- {
- "version": 54,
- "downLoadPath": "http://obfgb7oet.bkt.clouddn.com/patch106.apk",
- "fileName": "patch106.apk",
- "viewInfo": [
- {
- "packageName": "com.kot32.testdynamicviewproject.snow.widgets.SnowingView",
- "uuid": "activity_frame",
- "layoutParams": {
- "width": -1,
- "height": -1
- } },
- {
- "packageName": "com.kot32.testdynamicviewproject.player.MyPlayer",
- "uuid": "activity_player",
- "layoutParams": {
- "width": -1,
- "height": -1
- } }
- ]}
我們聲明了這次在線包的版本,每個(gè)View 的包名和布局參數(shù), 以及最重要的 和宿主程序中聲明對(duì)齊的uuid。
另外,Dynamic-load-view 能夠動(dòng)態(tài)加載外部apk中的View以及資源,能夠熱修復(fù)線上View,以及模塊化更新。
屏幕截圖
特點(diǎn)
- 插件程序完全獨(dú)立于宿主。
- 以 View作為模塊進(jìn)行模塊化開(kāi)發(fā)更新。
- 你也可以把View 鋪滿(mǎn)整個(gè)Activity,相當(dāng)于更新Activity。
- 副作用小,沒(méi)有加載Activity 帶來(lái)的生命周期等問(wèn)題。
- 兼容性好。Android 4.0~6.0 都沒(méi)有問(wèn)題。
- 簡(jiǎn)單。核心代碼不超過(guò)400行??梢宰孕邢螺d源碼,修改更新規(guī)則。
如何使用
- 下載庫(kù),并作為library 引用。
- 需要在宿主程序的Application 的onCreat 中初始化,代碼如下:.
- DynamicViewConfig config = new DynamicViewConfig.Builder()
- .context(this)
- .getUpdateInfoApi("http://vpscn.ifancc.com/php/dynamicView.php")
- .build();
- DynamicViewManager.getInstance(config).init();
getUpdateInfoApi 這個(gè)方法需要傳入一個(gè)API地址,這個(gè)API地址給客戶(hù)端提供更新的信息. 在上面的地址中,服務(wù)器返回了下面這樣的JSON 串:
- {
- "version": 39,
- "downLoadPath": "http://obfgb7oet.bkt.clouddn.com/patch101.apk",
- "fileName": "patch101.apk",
- "viewInfo": [
- {
- "packageName": "com.kot32.testdynamicviewproject.MyButton",
- "uuid": "test",
- "layoutParams": {
- "width": 100,
- "height": 100
- }
- },
- {
- "packageName": "com.kot32.testdynamicviewproject.MyButton1",
- "uuid": "test_activity",
- "layoutParams": {
- "width": -1,
- "height": -1
- }
- }
- ]
- }
上面的JSON 串定義了本次更新的版本以及更新包的地址,并且提供了對(duì)每個(gè)View 的詳細(xì)更新信息。
packageName :插件APK 中View 的完整包名.
uuid : 和宿主程序中待更新 View 相同的 UUID.
layoutParams:布局參數(shù).
你也可以自己修改服務(wù)器需要提供的參數(shù),更改com.kot32.dynamicloadviewlibrary.model 包中的模型類(lèi)即可。
- 待更新的View 需要xml 布局文件中如下聲明.注意uuid 屬性必須賦值。更新時(shí)會(huì)匹配uuid 相同的View。
- <com.kot32.dynamicloadviewlibrary.core.DynamicViewGroup
- android:id="@+id/dv"
- android:layout_width="200dp"
- android:layout_height="200dp"
- app:uuid="test"
- android:layout_centerInParent="true">
- <!--default view -->
- <ImageView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:src="@mipmap/ic_launcher" />
- </com.kot32.dynamicloadviewlibrary.core.DynamicViewGroup>
對(duì)于插件程序,只需要定義View 就好了,之后直接打成APK 包即可。
更多詳細(xì)信息,請(qǐng)直接下載示例源碼查看,源碼不多,也很好理解。
缺陷
- 現(xiàn)在可以加載插件程序中的string和drawable 資源,但是style.xml 和 dimens.xml 的加載還存在一些問(wèn)題。
- 插件程序中的資源文件的名字最好不要和主程序中重復(fù)。
- 在插件中訪問(wèn)資源請(qǐng)使用:getContext().getResources()