android之换肤原理解读

如下是解读demo的链接,自行下载

https://github.com/fengjundev/Android-Skin-Loader

由于是开源的,而且对于想了解换肤功能的童鞋这个demo实在是通俗易懂,原理也很清晰,所以忍不住想要记录一下,

题外话:附上一篇换肤技术总结的博客,这是一篇动态换肤与本地换肤(传统的theme)换肤优劣势的详细比较,需要的童鞋,可以详细拜读,至少知道来源

http://blog.zhaiyifan.cn/2015/09/10/Android%E6%8D%A2%E8%82%A4%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93/

换肤功能用于公司的运营是常有的需求,毕竟皮肤对于app来说还是比较重要的;这个对于开发者来说并不关心, 我们只关心其技术原理。

一、换肤功能:

解读的是一篇动态加载资源从而达到换肤 的效果,这也是换肤的一种潮流,行业上得换肤跟这个demo基本都大同小异,比较性能来说这个方案也是比较值得推荐

既然是动态的,那就支持网络加载的资源、本地资源应该都要支持,而且是无缝隙,高性能的,废话不多说,先来看看设计者的思路

二、思路:

前提:这demo只是用本地讲解 所以将要换肤的资源搞成apk,重新命名后缀为.skin,并保存在指定的sd目录中,方便加载,开发者也可通过网上下载保存到指定的sd目,方便支持线上换肤

1、当点击换肤时,使用AsyncTask加载已保存在sd目录中的apk,并通过AssetManager反射添加资源路径的方法将apk的资源加载进去,然后通过new Resource将Assetmanager管理器注册,得到一个全新的资源管理者AssetManager,通过这个管理者与app原生的资源管理者作为区分,当加载资源的时候,就可以通过资源管理者的资源作为换肤,所以需要护肤时将这个新对象resource将其赋值给全局的resource,通过Resource对象实现换肤;若切换回默认app的皮肤时,就将默认app生成的resource赋值给resource,在其获取资源时,通过resource来控制是取得哪一套资源,从而实现换肤。

2、当点击换肤,获取到resource之后,这里通过观察者模式去通知当前活动的页面进行换肤,而不是放在onResume实时监测,使用观察者就需要activity实现接口,这里通过在BaseActivity实现统一接口ISkinUpdate,统一进行注册,达到方便管理,方便换肤

3、需要换肤就得知道哪些view需要换肤,通过设置inflate中的一个工厂Factory,这个工厂是用来创建一个view,有点类似hook,只要这个Factory返回一个view就不会再进行解析我们xml设置的view,每创建一个view之前factory都会执行一次,所以在这里通过设置自己自定义的实现接口Factory的SkinInflateFactory,就可以在其读取layout的xml文件生成view之前会执行onCreateView,通过hook这个点,即生成xml文件的view又可以满足我们所要读取需要换肤的view,并且判断当前view是否需要换肤,需要则直接设置相应的color或drawable。到此基本就这个思路

三、代码走读

接下来一起看看代码走读:我的阅读习惯是从点击切换皮肤开始,然后一层层剥皮,需要用到的属性在哪里初始化,就跳转到哪里看看初始化的地方;读者在读demo的时候根据自己的习惯吧,这里就姑且按我的思维方式走。

换肤嘛,当然是找到点击换肤的事件咯;

找到如下类及其点击换肤响应事件的方法

public class SettingActivity extends BaseActivity {

   /**
    * Put this skin file on the root of sdcard
    * eg:
    * /mnt/sdcard/BlackFantacy.skin
    */
   private static final String SKIN_NAME = "BlackFantacy.skin";
   private static final String SKIN_DIR = Environment
         .getExternalStorageDirectory() + File.separator + SKIN_NAME;

   private void initView() {

      setNightSkinBtn.setOnClickListener(new OnClickListener() {

         @Override
         public void onClick(View v) {
            onSkinSetClick();
         }
      });

      setOfficalSkinBtn.setOnClickListener(new OnClickListener() {

         @Override
         public void onClick(View v) {
            onSkinResetClick();
         }
      });
   }

   private void onSkinSetClick() {
      if(!isOfficalSelected) return;

      File skin = new File(SKIN_DIR);

      if(skin == null || !skin.exists()){
         Toast.makeText(getApplicationContext(), "请检查" + SKIN_DIR + "是否存在", Toast.LENGTH_SHORT).show();
         return;
      }

      SkinManager.getInstance().load(skin.getAbsolutePath(),
            new ILoaderListener() {
               @Override
               public void onStart() {
                  L.e("startloadSkin");
               }

               @Override
               public void onSuccess() {
                  L.e("loadSkinSuccess");
                  Toast.makeText(getApplicationContext(), "切换成功", Toast.LENGTH_SHORT).show();
                  setNightSkinBtn.setText("黑色幻想(当前)");
                  setOfficalSkinBtn.setText("官方默认");
                  isOfficalSelected = false;
               }

               @Override
               public void onFailed() {
                  L.e("loadSkinFail");
                  Toast.makeText(getApplicationContext(), "切换失败", Toast.LENGTH_SHORT).show();
               }
            });
   }
}

然后在这个设置类里边,根据响应事件,我们看到了加载皮肤的调用处:

SkinManager.getInstance().load(skin.getAbsolutePath(),

当然路径是如下:先不管,将demo的BlackFanTancy.skin放到sd卡就行

private static final String SKIN_NAME = "BlackFantacy.skin";
   private static final String SKIN_DIR = Environment
         .getExternalStorageDirectory() + File.separator + SKIN_NAME;

接下来看看SkinManager这个单例皮肤管理类:其中load的方法,进入如下:

public void load(String skinPackagePath, final ILoaderListener callback) {

		new AsyncTask<String, Void, Resources>() {

			protected void onPreExecute() {
				if (callback != null) {
					callback.onStart();
				}
			};

			@Override
			protected Resources doInBackground(String... params) {
				try {
					if (params.length == 1) {//加载皮肤包,并且将包的路径加到资源管理器中AssetManager
						String skinPkgPath = params[0];

						File file = new File(skinPkgPath);
						if (file == null || !file.exists()) {//
							Log.d(TAG, "!file.exists() skinPkgPath= " + skinPkgPath);
							Log.d(TAG, "!file.exists() = " + file.exists());
							return null;
						}
						Log.d(TAG,"skinPkgPath = "+skinPkgPath);
						PackageManager mPm = context.getPackageManager();
						PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
						skinPackageName = mInfo.packageName;

						AssetManager assetManager = AssetManager.class.newInstance();
						Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
						addAssetPath.invoke(assetManager, skinPkgPath);

						Resources superRes = context.getResources();
						Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());

						SkinConfig.saveSkinPath(context, skinPkgPath);

						skinPath = skinPkgPath;
						isDefaultSkin = false;
						return skinResource;
					}
					return null;
				} catch (Exception e) {
					e.printStackTrace();
					return null;
				}
			};

			protected void onPostExecute(Resources result) {
				mResources = result;

				if (mResources != null) {
					if (callback != null) callback.onSuccess();
					notifySkinUpdate();
				}else{
					isDefaultSkin = true;
					if (callback != null) callback.onFailed();
				}
			};

		}.execute(skinPackagePath);
	}

这个方法传入了一个回调接口ILoadListner,以及皮肤绝对路径,随后通过AsyncTask异步执行加载路径上的skin资源

接下来看看其中三个方法

首先、onStart():没做什么工作,就是在加载之前判断callBack是否为空做一些初始化,我们这里没做什么初始化,只是打印而已

再次:doInBackground:重点的都在这个方法里边了:这个方法异步加载了资源,通过新创建Assetmanager与Resource建立对新加载的资源skin的管理,完成后通过SkinConfig.saveSkinPath();保存当前皮肤路径,以备下次再次打开app时默认加载皮肤还是上一次选中的。

最后:onPostExecut回到主线程处理更新皮肤;这里将新创建的resource对象保存到全局,由于callBack不为null,然后通过回调接口callBack.onSuccess()修改ui,以及调用notifySkinUpdate();猜测这个方法就是进行皮肤更新的方法。

到了这里这个线路基本完事儿;

细心好奇的你疑问肯定有两点:

1)、context是在哪儿赋值的

2)、notifySkinUpdate()到底做了什么工作

先来看看context到底在哪儿赋值的,当然是在当前类找了:你会发现下边这个方法

public void init(Context ctx){
   context = ctx.getApplicationContext();
}

这个很自然的看看其在哪儿调用的,快捷键ctrl+alt+H,只有一处方法调用,那就是Application,这是应用启动就初始化的如下

public class SkinApplication extends Application {

   public void onCreate() {
      super.onCreate();

      initSkinLoader();
   }

   /**
    * Must call init first
    */
   private void initSkinLoader() {
      SkinManager.getInstance().init(this);
      SkinManager.getInstance().load();
   }
}

看到这里,你会意外发现,咦,这里也有个load()那我们就会疑问这个load()是干啥用的,初始化的时候为何要调用它,这个load是在我们进入app时调用的,那就有理由猜想如下:

1)、 上次登录app时我们还没切换过皮肤,还是默认皮肤,这个load是怎么工作的

2)、上次登录app时我们切换皮肤了,那么这个load又是怎么工作的

带着这两个疑问,我们进入load()方法一探究竟呗

public void load(){
   String skin = SkinConfig.getCustomSkinPath(context);
   Log.d(TAG, "skin = " + skin);
   load(skin, null);
}

首先从sharePreference获取皮肤路径:分两步走

<一>、没切换过皮肤,则skin得到的是默认

public  static final String     DEFALT_SKIN          =  "cn_feng_skin_default";

<二>、切换了皮肤,则获取的是切换皮肤的路径:

然后往下走,神奇的发现调用了load(skin,null);  这个方法不就是前边我们分析过的吗,接下来我们再次看看这个方法,毕竟参数不一样了嘛:

/**
	 * Load resources from apk in asyc task
	 * @param skinPackagePath path of skin apk
	 * @param callback callback to notify user
	 */
	public void load(String skinPackagePath, final ILoaderListener callback) {

		new AsyncTask<String, Void, Resources>() {

			protected void onPreExecute() {
				if (callback != null) {
					callback.onStart();
				}
			};

			@Override
			protected Resources doInBackground(String... params) {
				try {
					if (params.length == 1) {//加载皮肤包,并且将包的路径加到资源管理器中AssetManager
						String skinPkgPath = params[0];

						File file = new File(skinPkgPath);
						if (file == null || !file.exists()) {//
							Log.d(TAG, "!file.exists() skinPkgPath= " + skinPkgPath);
							Log.d(TAG, "!file.exists() = " + file.exists());
							return null;
						}
						Log.d(TAG,"skinPkgPath = "+skinPkgPath);
						PackageManager mPm = context.getPackageManager();
						PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
						skinPackageName = mInfo.packageName;

						AssetManager assetManager = AssetManager.class.newInstance();
						Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
						addAssetPath.invoke(assetManager, skinPkgPath);

						Resources superRes = context.getResources();
						Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());

						SkinConfig.saveSkinPath(context, skinPkgPath);

						skinPath = skinPkgPath;
						isDefaultSkin = false;
						return skinResource;
					}
					return null;
				} catch (Exception e) {
					e.printStackTrace();
					return null;
				}
			};

			protected void onPostExecute(Resources result) {
				mResources = result;

				if (mResources != null) {
					if (callback != null) callback.onSuccess();
					notifySkinUpdate();
				}else{
					isDefaultSkin = true;
					if (callback != null) callback.onFailed();
				}
			};

		}.execute(skinPackagePath);
	}

当然还是分两步走:由于callback为null,所以不在走callback回调,这里不做分析了

1)默认皮肤时,skin为默认的

public  static final String     DEFALT_SKIN          =  "cn_feng_skin_default";

由于这个是不存在的,所以当执行到doInBackground():

File file = new File(skinPkgPath);
						if (file == null || !file.exists()) {//
							Log.d(TAG, "!file.exists() skinPkgPath= " + skinPkgPath);
							Log.d(TAG, "!file.exists() = " + file.exists());
							return null;
						}

在这里会直接返回,不在加载皮肤;用的就是app中layout默认的颜色背景;到这里这个默认的分析完毕

2)切换了皮肤:前边也分析保存了皮肤在sd中的绝对路径:所以这里获取的skin路径上一次皮肤的路径

再往下走,除了callback不调用外,最后还是调用notifySkinUpdate()方法;

那么这个方法到底做了什么,肯定就是我们的换肤方法了;好奇的你肯定会进入notifySkinUpdate()方法一探究竟,那我们就一起看看:

@Override
	public void notifySkinUpdate() {
		if(skinObservers == null) return;
		for(ISkinUpdate observer : skinObservers){
			observer.onThemeUpdate();
		}
	}

当打开app的时候,不管曾经是否换肤,由于skinObservers为null,所以直接返回

那么我们就要看看这个观察者skinObservers在哪儿初始化,哪儿订阅的了;

我们会发现初始化的方法、订阅的地方以及取消订阅的地方如下:

@Override
	public void attach(ISkinUpdate observer) {
		if(skinObservers == null){
			skinObservers = new ArrayList<ISkinUpdate>();
		}
		if(!skinObservers.contains(skinObservers)){
			skinObservers.add(observer);
		}
	}

	@Override
	public void detach(ISkinUpdate observer) {
		if(skinObservers == null) return;
		if(skinObservers.contains(observer)){
			skinObservers.remove(observer);
		}
	}

这一看这里都是重写的方法,细心的你会发现notifySkinUpdat()方法也是重写的,那么我们就看看它实现的接口

public class SkinManager implements ISkinLoader{

然后再看看attach(ISkinUpdate observer)和detach(ISkinUpdate observer)在哪儿调用,在查看之前我们有理由猜想,BaseActivity肯定是作为观察者实现了ISkinUpdate,已实时监测换肤功能;

接下来查看attach(ISkinUpdate observer)和detach(ISkinUpdate observer)调用:发现确实是BaseActivity和BaseFragementActivity两个类中调用:如下

@Override
protected void onResume() {
   super.onResume();
   SkinManager.getInstance().attach(this);
}

@Override
protected void onDestroy() {
   super.onDestroy();
   SkinManager.getInstance().detach(this);
   mSkinInflaterFactory.clean();
}

传的是this,那么她们肯定实现了接口

public interface ISkinUpdate {
   void onThemeUpdate();
}

自然而然,我们就来看看onThemeUdapte做了什么,它就能更换皮肤了?

@Override
public void onThemeUpdate() {
   if(!isResponseOnSkinChanging){
      return;
   }
   mSkinInflaterFactory.applySkin();
}
isResponseOnSkinChanging

这个默认是true,也没地方改变它的默认值,我们先不管,直接跳到下面那行

   mSkinInflaterFactory.applySkin();

还是看看mSkinInflaterFactory到底是什么鬼,在哪儿初始化的,查看知道在oncreate方法中:

private SkinInflaterFactory mSkinInflaterFactory;

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   mSkinInflaterFactory = new SkinInflaterFactory();
   getLayoutInflater().setFactory(mSkinInflaterFactory);
}

这到底是什么鬼,每次进入activity都要设置这个Factory,这里我们部队Factory开讲,下次再进行对它的源码深究,我们只要知道它是一个生产view的工厂类,在inflate的时候,通过每遍历一个layout的每个组件view之前都会检测Factory是否为null,若不为null,则会调用onCreaterView();

扩展:Factory是否要生成view,如果生成view,则不会在创建layout遍历的那个组件,所以通过这个Factory也可以更改返回显示的view:比如layout布局其中一个组件是Imageview,而通过Factory可以生成TextView代替ImageView;

接下来看看SkinInFlaterFactory这个类的onCreateView,这个类肯定实现了Factory接口,否则setFactory()的,所以每次在inFlate时由于factory不为空,肯定都会检测是否要调用onCreateView;所以setFactory必须设置在setContentView()方法之前;因为setContentView实际上也是调用inflate;

那就看看其oncreateView()咯:

@Override
	public View onCreateView(String name, Context context, AttributeSet attrs) {
		// if this is NOT enable to be skined , simplly skip it
		//在xml的节点中设置,设置为true表示是需要换肤的view,否则跳过这个view,因为这个view不需要换肤
		boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        if (!isSkinEnable){
        		return null;
        }

		View view = createView(context, name, attrs);

		if (view == null){
			return null;
		}

		parseSkinAttr(context, attrs, view);

		return view;
	}

首先:就来看看这个isSkinEnable是什么:这个是我们在layout为组件设置的一个标志,标着这个组件是需要换肤的,如果不需要换肤的组件我们就不用往下走

其次:假设在组件中设置属性skin:enable=true,则就会往下执行 ,执行createView()以及parseSkinAttr(),以下分析这连个个方法:

private View createView(Context context, String name, AttributeSet attrs) {
		View view = null;
		try {
			Log.d("SkinInflaterFactory","name = "+name);
			if (-1 == name.indexOf('.')){//-1则不是自定义的view
				if ("View".equals(name)) {
					view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
				}
				if (view == null) {
					view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
				}
				if (view == null) {
					view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
				}
			}else {
	            view = LayoutInflater.from(context).createView(name, null, attrs);
	        }

			L.i("about to create " + name);

		} catch (Exception e) {
			L.e("error while create 【" + name + "】 : " + e.getMessage());
			view = null;
		}
		return view;
	}

这里返回一个创建view,主要是判断:

这个组件view是否是用原生的还是自定义的:name是组件的名字:如TextView、ImageView,所示自定义的,则得到的是全名(包名+类名)

parseSkinAttr():

private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
		List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();

		for (int i = 0; i < attrs.getAttributeCount(); i++){
			String attrName = attrs.getAttributeName(i);
			String attrValue = attrs.getAttributeValue(i);
			Log.d(TAG,"attrName = "+attrName);//属性name比如layout_width或者自定义属性name比如本次用的enable 值是true
			Log.d(TAG,"attrValue = "+attrValue);//属性的值
			if(!AttrFactory.isSupportedAttr(attrName)){
				continue;
			}

		    if(attrValue.startsWith("@")){
				try {
					int id = Integer.parseInt(attrValue.substring(1));
					String entryName = context.getResources().getResourceEntryName(id);
					String typeName = context.getResources().getResourceTypeName(id);
					SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
					Log.d(TAG,"id = "+id);
					Log.d(TAG,"getResourceEntryName = "+entryName);//color的name或drawable的name
					Log.d(TAG,"getResourceTypeName = "+typeName);//比如:color,drawable
					if (mSkinAttr != null) {
						viewAttrs.add(mSkinAttr);
					}
				} catch (NumberFormatException e) {
					e.printStackTrace();
				} catch (NotFoundException e) {
					e.printStackTrace();
				}
		    }
		}

		if(!ListUtils.isEmpty(viewAttrs)){
			SkinItem skinItem = new SkinItem();
			skinItem.view = view;
			skinItem.attrs = viewAttrs;

			mSkinItems.add(skinItem);

			if (SkinManager.getInstance().isExternalSkin()) {
				//这里是每次进入activity后fragment的时候都要判断是否需要换肤
				skinItem.apply();
			}
		}
	}

这个方法是解析组件的所有属性,并将得到的可以换肤的所有属性color或drawable属性id和属性名保存到一个viewAttrs,然后将viewAttrs和view所有相关值,保存skinItem,随后将SkinItem缓存到mSkinItem集合中,接下来是判断当前是否需要换肤;假设是需要的,则我们来看看它是如何执行的

public void apply(){
		if(ListUtils.isEmpty(attrs)){
			return;
		}
		for(SkinAttr at : attrs){
			at.apply(view);
		}
	}

由于parseSKinAttr解析式已经将attrs设置,所不会为空,所以会执行for循环

查看at.apply(View),那么我们发现SkinAttr是个抽象类,抽象方法为apply(view),于是回过头看看parseSKinAttr这个方法在哪里调用的:惊奇的发现:

SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);

用的竟让是个工厂方式,进去一瞧咯:

public static SkinAttr get(String attrName, int attrValueRefId, String attrValueRefName, String typeName){

   SkinAttr mSkinAttr = null;

   if(BACKGROUND.equals(attrName)){
      mSkinAttr = new BackgroundAttr();
   }else if(TEXT_COLOR.equals(attrName)){
      mSkinAttr = new TextColorAttr();
   }else if(LIST_SELECTOR.equals(attrName)){
      mSkinAttr = new ListSelectorAttr();
   }else if(DIVIDER.equals(attrName)){
      mSkinAttr = new DividerAttr();
   }else{
      return null;
   }

   mSkinAttr.attrName = attrName;
   mSkinAttr.attrValueRefId = attrValueRefId;
   mSkinAttr.attrValueRefName = attrValueRefName;
   mSkinAttr.attrValueTypeName = typeName;
   return mSkinAttr;
}

很容易就知道,这是生成什么属性,支持哪些换肤,主要有四个,所以只拿第一个BackgroundAttr类作为分析,其它原理一样:

public class BackgroundAttr extends SkinAttr {

   @Override
   public void apply(View view) {

      if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
         view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
         Log.i("attr", "_________________________________________________________");
         Log.i("attr", "apply as color");
      }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
         Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
         view.setBackground(bg);
         Log.i("attr", "_________________________________________________________");
         Log.i("attr", "apply as drawable");
         Log.i("attr", "bg.toString()  " + bg.toString());

         Log.i("attr", this.attrValueRefName + " 是否可变换状态? : " + bg.isStateful());
      }
   }
}

原来换肤最终的真相在这里:

view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId))

以及

 Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
         view.setBackground(bg);

这里边的

SkinManager.getInstance().getColor(attrValueRefId)

SkinManager.getInstance().getDrawable(attrValueRefId)

做了什么:进去瞧瞧就知道了:这里选择一个来分析吧,其它都一样的

public int getColor(int resId){
   int originColor = context.getResources().getColor(resId);
   if(mResources == null || isDefaultSkin){
      return originColor;
   }
   //通过默认的resId获取默认颜色的资源名,通过名字查找皮肤包一致的名字再获取生成的dstId
   String resName = context.getResources().getResourceEntryName(resId);

   int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
   int trueColor = 0;

   try{
      trueColor = mResources.getColor(trueResId);
   }catch(NotFoundException e){
      e.printStackTrace();
      trueColor = originColor;
   }

   return trueColor;
}

首先:通过app的context获取originColor,app的默认的颜色,若mResoutces=null(没切换皮肤)或isDefaultSkin=true(显示的是默认的),则直接返回显示默认color

否则:通过默认的resId获取默认颜色的资源名,通过名字查找皮肤包一致的名字再获取生成的dstId,得到dstId这里就是trueResId,这个id就是从新的Resource资源管理者获取的,就是换肤的皮肤颜色id,这样就能获得了皮肤,直接返回设置颜色就可以换肤成功了;

到这里终于结束了!!!,理解有误的地方,敬请指正!!!

时间: 2024-11-04 17:06:04

android之换肤原理解读的相关文章

Android中插件开发篇之----应用换肤原理解析

一.前言 今天又到周末了,感觉时间过的很快呀.又要写blog了.那么今天就来看看应用的换肤原理解析.在之前的一篇博客中我说道了Android中的插件开发篇的基础:类加载器的相关知识.没看过的同学可以转战: http://blog.csdn.net/jiangwei0910410003/article/details/41384667 二.原理介绍 现在市场上有很多应用都有换肤的功能,就是能够提供给用户一些皮肤包,然后下载,替换.而且有些皮肤是要收费的.对于这个功能的话,其实没有什么技术难度的,但

Android实现换肤功能(一)

上周有个朋友给建议说讲讲换肤吧,真巧这周公司的工作安排也有这个需求,换的地方之多之繁,让人伤神死了.正所谓磨刀不误砍柴工,先磨下刀,抽出一个工具类,写了个关于换肤的简单demo. Android中换肤的实现有几种方法,我使用的是读取共享进程空间另一apk中的资源的方法.大致的原理如下,让你的app和一个只有资源文件的app运行在同一进程中,这样你的app就可以访问另一app的资源了.原理很简单,下面我们分步骤讲解实现过程. 一.共享进程运行的空间 通过在两个工程下的manifest根节点添加相同

Android主题换肤 无缝切换

2016年7月6日 更新:主题换肤库子项目地址:ThemeSkinning,让app集成换肤更加容易.欢迎star以及使用,提供改进意见. 更新日志: v1.3.0:增加一键切换切换字体(初版)v1.2.1:完善之前版本View的创建v1.2.0:增加对换肤属性自定义扩展v1.1.0:可以直接加载网络上的皮肤文件 今天再给大家带来一篇干货. Android的主题换肤 ,可插件化提供皮肤包,无需Activity的重启直接实现无缝切换,可高仿网易云音乐的主题换肤. 这个链接是本次的Demo打包出来的

Android动态换肤开源库Colorful发布

最近本人需要用到夜间模式,但是经过一番搜索似乎并没有看到好的开源实现,看到有一个类似的库MultipleTheme,但是需要自定义所有要实现换肤功能的View,感觉比较麻烦.当发现现有的解决方案不能很好的解决问题时,往往只能自己实现,因此本人花了点时间简单弄了一个实现该功能的开源库,命名为Colorful. Colorful是基于Theme,无需重启Activity.无需自定义View,方便的实现日间.夜间模式,github地址为 https://github.com/bboyfeiyu/Col

Android实现换肤功能(二)

前两天写的上章关于换肤的功能获得了很好的反响,今天为大家介绍另一种方式.今天实现的策略也是网友建议的,然后我自己去写了个demo,大家自己评估下相比第一种方式的优势和劣势在哪里. 简单介绍下关于第一种关于换肤实现的方法一些不友好的地方,比如点击了换肤的功能键,需要先下载一个资源apk,然后再去引用其中的资源ID,于用户使用起来确实有有很多不便之处.今天为大家介绍的方法是克服了这一弊端的,可以实现直接在应用内切换,把资源文件存储在本地apk的,读取和操作也是更加便捷的,下面介绍下大致的一个实现逻辑

Android动态换肤(一、应用内置多套皮肤)

动态换肤在很多android应用中都有使用,用户根据自己的喜好设置皮肤主题,可以增强用户使用应用的舒适度. Android换肤可以分为很多种,它们从使用方式,用户体验以及项目框架设计上体现了明显的差异. 接下来几篇文章分别讲解其中比较主流的换肤方式. 应用内置皮肤实现动态切换在技术上是最容易实现的,但有很多局限性,比如不能在使用过程中增减皮肤,除非升级应用,扩展性很弱:如果需要设置皮肤的位置很多,编码起来比较麻烦.主要是使用 SharedPreferences记录当前设置的皮肤序号,然后加载这套

200行代码打造超越一线互联网公司的换肤架构

本专栏专注分享大型Bat面试知识,后续会持续更新,喜欢的话麻烦点击一个关注 面试官: 网易云QQ的换肤是怎么做到的,你对换肤有了解吗?看过换肤的原理没? 心理分析:没有接触过换肤技术 第一次听到该名词肯定会很茫然.面试官考的是对资源加载,监听布局,有没有了解.本文从换肤实战一对一讲解.告诉你如何做以及实现.文章末尾带换肤项目源码 求职者: 从监听布局开始到 换肤原理,详细给面试官讲解换肤的原理 接下来我们一起分享这篇干货. Android的主题换肤 ,可插件化提供皮肤包,无需Activity的重

android 换肤模式总结

由于Android的设置中并没有夜间模式的选项,对于喜欢睡前玩手机的用户,只能简单的调节手机屏幕亮度来改善体验.目前越来越多的应用开始把夜间模式加到自家应用中,没准不久google也会把这项功能添加到Android系统中吧. 业内关于夜间模式的实现,有两种主流方案,各有其利弊,我较为推崇第三种方案: 1.通过切换theme来实现夜间模式.2.通过修改uiMode来切换夜间模式. 3.通过插件方式切换夜间模式. 值得一提的是,上面提到的几种方案,都是资源内嵌在Apk中的方案,像新浪微博那种需要通过

教程: Android应用如何实现换肤功能

本节课程的目的:学会换肤的方案及实现,掌握不同换肤方案的优缺点及适用场合. 希望各位同学做到:学习某一个技巧就掌握透彻,多练习.最好举一反三.触类旁通,掌握分析问题解决问题的思路和方法. 我讲解的是原理以及实现的关键技术点,细节.优化及与课程主题不相关的或初级的内容可能不会讲解.这是免费培训,精力有限,暂时只能做到让普通的变优秀,让优秀的变卓越.你要是已经卓越了来教教我吧.暂时不做入门培训. 国内有很多的软件都支持皮肤定制,这也是与国外软件重大不同之一,国外用户注重社交.邮件等功能,国内用户则重