利用动态加载实现手机淘宝的节日特效

相信去年圣诞节打开过手机淘宝的童鞋都会对当时的特效记忆犹新吧:全屏飘雪,旁边还有个小雪人来控制八音盒背景音乐的播放,让人有种身临其境的感觉,甚至忍不住想狠狠购物了呢(误),大概就是下面这个样子滴:


嗯,确实很炫,那么我们一步步去分析是如何实现的:

一、实现下雪的 View

首先,最上面一层的全屏雪花极有可能是一个顶层的View,而这个View是通过动态加载去控制显示的(不更新淘宝也能看到这个效果)。那么我们先得实现雪花效果的 View,人生苦短,拿来就用。打开 gank.io,搜索"雪花":


看样子第7个库就是我们想要的了,点进源码,直接 download 不解释,记得 star 一个支持作者。那么现在我们的项目中就有一个完整的下雪效果 View 了。

二、实现雪人播放器 View

这个一张雪人图片+一个按钮即可实现,就不多解释了。接下来需要一段圣诞节音频,直接进行在线音频播放无疑是节省空间的好方案。『我的滑板鞋』烘托出的寂寞而甜蜜的氛围无疑是最适合圣诞节的,因此我们得到了『神曲』URL 一枚:
http://cdn.ifancc.com/TomaToDo/bgms/my_hbx.mp3
接下来要找一个小雪人的图片当作播放器的背景,那么阿姆斯特朗...不对,是这个:


嗯,相当可爱喜庆。那么播放器核心代码如下:

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(), "开始播放", Toast.LENGTH_SHORT).show();
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!mediaPlayer.isPlaying()) {
                    mediaPlayer.start();
                    Toast.makeText(getContext(), "继续播放", 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之后自动播放
            mediaPlayer.start();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        try {
            mediaPlayer.stop();
            mediaPlayer.release();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

三、动态加载思路

上面基本实现了在本地的雪花以及播放音乐效果,那么在不更新主程序的情况下,如何将这两个View动态加载到主程序当中去呢?

首先我们明白,Android 的DexClassloader 是拥有加载任意APK 中任意类的能力的,只是有以下限制:

  • 加载出的Activity 由于不在宿主 Manifest 文件中声明,因此框架无法找到并初始化这个Activity。
  • 加载出的Activity 不具备生命周期,理由同上。
  • 加载出的类的Resource 文件id 会和主程序混淆在一起。

由于我们只是加载View,并不是加载整个Activity,所以前两个问题并不会遇到,而第三个问题可以想办法解决掉。
在主程序中我们也要做这三件事:

  • 把能够装载View的ViewGroup 的空位留出来
  • 去获取更新的patch包
  • 把View 从apk包中加载出来之后,放进留好的ViewGroup 中。
    这样一来,不仅是圣诞节,在之后的各种活动上都可以在线去加载活动的View。

四、开始加载

在加载View 之前,首先要意识到这个View 是引用了图片资源的(小猪图片),因此我们要解决资源问题:

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原来的资源管理器,也就是说现在可以在宿主中访问插件资源了。

核心加载代码如下:

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);

中间对 mResources 的操作的作用是:将宿主的Activity 的mResources 重置,避免在Activity 中使用资源时和插件冲突。

然而机智的我已经把更新包下载、版本管理、动态加载都封装好了,所以正确的加载方式是:
引用它: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="原始页面"
            />

    </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>

以上声明了主界面的布局,当然,在动态加载之前除了原有的"原始页面"TextView,是不会有任何其他东西的,也就是圣诞节来临之前的程序。注意:uuid 会和在线包相匹配。

2.打插件包

其实就是把之前包含了我们所写的两个View(雪花和雪人)的程序打包成apk。可以不签名。

3.把插件包放到服务器

在服务器返回的JSON中声明插件包地址和动态View 的一些参数,这里的演示程序请求地址为:
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
      }
    }
  ]
}

我们声明了这次在线包的版本,每个View 的包名和布局参数, 以及最重要的 和宿主程序中声明对齐的uuid

看看效果吧:

(播放的声音有点小,可以把声音开大一点聆听神曲)
http://www.miaopai.com/show/D7jWL6EeMAvs96O2f3qh1g__.htm
好啦,源码全部放在
https://github.com/kot32go/dynamic-load-view
这里了,提前祝大家圣诞快乐~



文/kot32(简书作者)
原文链接:http://www.jianshu.com/p/195eb1d8d0de
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值