Android换肤体验-------侧滑菜单的实现+换肤的原理

在我们使用APP时,几乎每个APP都有换肤功能,而对于不同的皮肤包,有些比较小,可以在应用内实现换肤,而有些比较大的皮肤包,继承到APP中占用内存极大,因此采用插件化的方式实现换肤。本节就从多个方式实现换肤体验。

1.关于侧滑菜单的实现

在使用QQ时,进行侧滑操作时,会进入到个人中心界面,这个就是通过侧滑菜单实现的,那么侧滑菜单的实现,是如何完成的?

(1)使用DrawerLayout和NavigationView实现

导入依赖:

implementation 'com.android.support:design:28.0.0'
implementation 'com.readystatesoftware.systembartint:systembartint:1.0.4'

NavigationView的特性:
在使用NavigationView的时候,尤其是在我们往xml布局添加NavigationView时,我们在将NavigationView放入布局的时候,我们之前设置的布局就不见了,不知你们是否有相似的体验。

这也是NavigationView的一个很重要的特性,尤其是在我们打开或者关闭侧滑菜单时,使用到的一个属性。NavigationView可以在侧拉菜单中添加头文件和Menu菜单,可以根据我们自己的设置往里添加。

主界面的布局:标题栏+ListView(LinearLayout)+侧拉菜单

<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawerlayout"
    tools:context=".view.activity.MainActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <include layout="@layout/title"></include>
        <ListView
            android:id="@+id/lv_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            ></ListView>
    </LinearLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nv_left"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="left"
        app:headerLayout="@layout/header"
        app:menu="@menu/menu_left"
        ></com.google.android.material.navigation.NavigationView>

</androidx.drawerlayout.widget.DrawerLayout>

一定要设置android:layout_gravity="left"这个属性,不然侧拉菜单会覆盖整个主界面,这样实现布局之后,基本就可以手动滑动侧拉菜单了。

在侧拉菜单中,我们可以设置Menu布局,添加想要的item。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <group android:id="@+id/group1" android:checkableBehavior="single">
        <item android:id="@+id/itme1"
            android:icon="@mipmap/emoji6"
            android:title="应用内换肤(Red)"></item>
        <item android:id="@+id/itme2"
            android:icon="@mipmap/emoji6"
            android:title="应用内换肤(Green)"></item>
        <item android:id="@+id/itme3"
            android:icon="@mipmap/emoji6"
            android:title="第三个Item"></item>
        <item android:id="@+id/itme4"
            android:icon="@mipmap/emoji6"
            android:title="插件式换肤1"></item>
        <item android:id="@+id/itme5"
            android:icon="@mipmap/emoji6"
            android:title="恢复默认"></item>
    </group>
</menu>

可以设置单击事件,响应每个Item。

 nv_left.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
                switch (menuItem.getItemId()){
                    case R.id.itme1:
                        Toast.makeText(MainActivity.this,"单击了第一个Item",Toast.LENGTH_SHORT).show();
                        break;
                    case R.id.itme2:
                        Toast.makeText(MainActivity.this,"单击了第二个Item",Toast.LENGTH_SHORT).show();
                        break;
                    case R.id.itme3:
                        Toast.makeText(MainActivity.this,"单击了第三个Item",Toast.LENGTH_SHORT).show();
                        break;
                    case R.id.itme4:
                        Toast.makeText(MainActivity.this,"单击了第四个Item",Toast.LENGTH_SHORT).show();
                        break;
                    case R.id.itme5:
                        Toast.makeText(MainActivity.this,"单击了第五个Item",Toast.LENGTH_SHORT).show();
                        break;
                }
                //点击Item后退出侧滑菜单
                drawerlayout.closeDrawer(GravityCompat.START);
                return true;
            }
        });

然后实现主界面的布局ListView,很简单就不在这里写了。

2.插件化更新皮肤

将.apk文件作为皮肤包,放在手机的SD卡中,从SD卡中获取想要的资源。
在这里插入图片描述
将.apk文件加载到SD卡中之后,就去读取插件中的资源文件。在我们去获取系统资源的时候,我们通过getResource()方法去获取,但是如果想去获取插件中的资源,我们使用getResource()是不可能获取到的,因为getResource()默认的资源地址是宿主APK的资源,那么我们只能去通过反射修改AssetManager的加载路径,通过getResource()来获取插件apk的资源,这个思想在插件化中是同样的思想。

在获取插件apk中的资源需要几个参数:插件apk的地址(保存在sd卡中)、插件apk的包名、AssetManager对象。

/**
     * 加载插件资源
     * @param plugin_path   插件的位置
     * @param plugin_packagename  插件包名
     */
    private void loadPlugin(String plugin_path, String plugin_packagename) {
        //创建AssetManager
        try {
            AssetManager manager = AssetManager.class.newInstance();
            Method addAssetPath = manager.getClass().getDeclaredMethod("addAssetPath", String.class);
            //传入插件的路径
            addAssetPath.invoke(manager,plugin_path);
            //获取系统Resource
            Resources superResources = getResources();
            //通过AssetManager得到了指定路径的资源
            Resources resources = new Resources(manager,superResources.getDisplayMetrics(),
                    superResources.getConfiguration());
            
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

得到这个resources对象,就是插件apk的资源对象,而不是宿主APK的资源对象,通过这个资源对象以及包名,就可以获取插件apk的资源。

public class ResourceManager {
    //插件资源
    private Resources mResources;
    //插件apk的包名
    private String mPackageName;

    public ResourceManager(Resources mResources, String mPackageName) {
        this.mResources = mResources;
        this.mPackageName = mPackageName;
    }

    public Drawable getDrawableByName(String name){
        try {
            return mResources.getDrawable(mResources.getIdentifier(name, "drawable", mPackageName));
        }catch (Exception e){
            e.getStackTrace();
            return null;
        }
    }
}

通过定义的资源管理类,得到插件apk中的资源

 //获取资源
            ResourceManager resourceManager = new ResourceManager(resources,plugin_packagename);
            Drawable bg1 = resourceManager.getDrawableByName("bg1");
            Log.e("TAG","bg1==="+bg1);
            if(bg1 != null){
                    drawerlayout.setBackground(bg1);
            }else{
                Toast.makeText(MainActivity.this,"更换背景失败",Toast.LENGTH_SHORT).show();
            }

3.捕获需要换肤的控件

在创建Activity的时候,会调用onCreate方法,初始化加载我们的布局,通过setContentView加载我们的布局。

@Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

通过代码可以看到,是通过getDelegate()方法调用了setContentView方法,getDelegate得到的是一个Delegate的实现类AppCompatDelegateImpl,是这个类调用了setContentView方法。

 @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

其实setContentview方法调用的是这个方法,当中有一个LayoutInflater类,这个我们经常使用,就是加载布局的类,通常使用inflate方法加载布局。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            final String name = parser.getName();
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
            return temp;
    }

真正生成布局的方法就是 createViewFromTag,根据Tag来生成View,Tag是什么?就是我们在XML布局文件中,添加控件时的第一个单词,例如“TextView”、“Button”、“EditText”等等;
createViewFromTag当中,会判断,LayoutInflater中接口的类型是Factory还是Factory2,现在一般都是Factory2

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            return view;
        } catch (Exception e) {
        }
    }

在这里插入图片描述
如上图所示,通过设置setFactory调用onCreateView,加载布局。如果没有使用LayoutInflater设置Factory加载布局,那么使用LayoutInflaterCompat加载布局。

    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

例如下载一段代码,在布局中,添加的是一个TextView,那么我们采用hook技术,截获系统加载布局的代码,让其返回一个EditText控件。

        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//        inflater.setFactory();
        LayoutInflaterCompat.setFactory(inflater, new LayoutInflaterFactory() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

                for (int i = 0; i < attrs.getAttributeCount(); i++) {
                    String attributeName = attrs.getAttributeName(i);
                    String attributeValue = attrs.getAttributeValue(i);
                    Log.e("TAG",attributeName+"=="+attributeValue);
                }
                if(name.equals("TextView")){
                    //如果得到的Tag是TextView,那么就返回创建一个TextView
                    return new EditText(context,attrs);
                }
                return null;
            }
        });

因此,通过这个方法,可以捕获整体布局中的全部控件,哪个控件换肤,我们可以得到该控件的属性,做修改。

那么怎么知道这个控件哪些属性需要换肤?我们可以为控件设置属性文件,以某个字母开头,比如“skin”,那么获取以这个单词名开头的属性,就是需要换肤的。在插件apk中存在我们的主题资源,在主APP中,假设某个控件它的background属性值是一个颜色值@Color/Primary,假设在插件apk中有这个资源名称与之对应,那么就更新这个控件的背景。

好了,换肤的底层原理就先讲到这里,之后会更新一下主题换肤的框架。

发布了16 篇原创文章 · 获赞 5 · 访问量 1458

猜你喜欢

转载自blog.csdn.net/qq_33235287/article/details/104231294