欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android?換膚實現(xiàn)指南demo及案例解析

 更新時間:2023年06月01日 09:43:57   作者:xiangzhihong  
這篇文章主要為大家介紹了Android換膚指南demo及案例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

一、換膚方案

目前,市面上Android的換膚方案主要有Resource方案和AssetManager替換方案兩種方案。

其中,Resource方案是用戶提前自定義一些主題,然后將指定主題對應的 id 設置成默認的主題即可。而AssetManager替換方案,使用的是Hook系統(tǒng)AssetMananger對象,然后再編譯期靜態(tài)對齊資源文件對應的id數值。

1.1 Resource方案

Resource方案的原理大概如下:

1、創(chuàng)建新的Resrouce對象(代理的Resource)

2、替換系統(tǒng)Resource對象

3、運行時動態(tài)映射(原理相同資源在不同的資源表中的Type和Name一樣)

4、xml布局解析攔截(xml布局中的資源不能通過代理Resource加載,LayoutInflater)

此方案的優(yōu)勢是支持String/Layout的替換,不過缺點也很明顯:

  • 資源獲取效率有影響
  • 不支持style、asset目錄

Resource多出替換,Resource包裝類代碼量大

1.2 AssetManager方案

使用的是Hook系統(tǒng)AssetMananger對象,然后再編譯期靜態(tài)對齊資源文件對應的id數值,達到替換資源的目的。此種方案,最常見的就是Hook LayoutInflater進行換膚。

二、Resource換膚

此種方式采用的方案是:用戶提前自定義一些主題,然后當設置主題的時候將指定主題對應的 id 記錄到本地文件中,當 Activity RESUME 的時候,判斷 Activity 當前的主題是否和之前設置的主題一致,不一致的話就調用當前 Activity 的recreate()方法進行重建。

比如,在這種方案中,我們可以通過如下的方式預定義一些屬性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
? ? <attr name="themed_divider_color" format="color"/>
? ? <attr name="themed_foreground" format="color"/>
? ? <!-- .... -->
</resources>

然后,在自定義主題中使用為這些預定義屬性賦值。

<style name="Base.AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
? ? <item name="themed_foreground">@color/warm_theme_foreground</item>
? ? <item name="themed_background">@color/warm_theme_background</item>
? ? <!-- ... -->
</style>

最后,在布局文件中通過如下的方式引用這些自定義屬性。

<androidx.appcompat.widget.AppCompatTextView
? ? android:id="@+id/tv"
? ? android:textColor="?attr/themed_text_color_secondary"
? ? ... />
<View android:background="?attr/themed_divider_color"
? ? android:layout_width="match_parent"
? ? android:layout_height="1px"/>

三、Hook LayoutInflater方案

3.1 工作原理

通過 Hook LayoutInflater 進行換膚的方案是眾多開源方案中比較常見的一種。在分析這種方案之前,我們最好先了解下 LayoutInflater 的工作原理。通常,當我們想要自定義 Layout 的 Factory 的時候可以調用下面兩個方法將我們的 Factory 設置到系統(tǒng)的 LayoutInflater 中。

public abstract class LayoutInflater {
? ? public void setFactory(Factory factory) {
? ? ? ? if (mFactorySet) throw new IllegalStateException("A factory has already been set on this LayoutInflater");
? ? ? ? if (factory == null) throw new NullPointerException("Given factory can not be null");
? ? ? ? mFactorySet = true;
? ? ? ? if (mFactory == null) {
? ? ? ? ? ? mFactory = factory;
? ? ? ? } else {
? ? ? ? ? ? mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
? ? ? ? }
? ? }
? ? public void setFactory2(Factory2 factory) {
? ? ? ? if (mFactorySet) throw new IllegalStateException("A factory has already been set on this LayoutInflater");
? ? ? ? if (factory == null) throw new NullPointerException("Given factory can not be null");
? ? ? ? mFactorySet = true;
? ? ? ? if (mFactory == null) {
? ? ? ? ? ? mFactory = mFactory2 = factory;
? ? ? ? } else {
? ? ? ? ? ? mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
? ? ? ? }
? ? }
}

當我們調用 inflator()方法從 xml 中加載布局的時候,將會走到如下代碼真正執(zhí)行加載操作。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
? ? synchronized (mConstructorArgs) {
? ? ? ? // ....
? ? ? ? final Context inflaterContext = mContext;
? ? ? ? final AttributeSet attrs = Xml.asAttributeSet(parser);
? ? ? ? Context lastContext = (Context) mConstructorArgs[0];
? ? ? ? mConstructorArgs[0] = inflaterContext;
? ? ? ? View result = root;
? ? ? ? try {
? ? ? ? ? ? advanceToRootNode(parser);
? ? ? ? ? ? final String name = parser.getName();
? ? ? ? ? ? // 處理 merge 標簽
? ? ? ? ? ? if (TAG_MERGE.equals(name)) {
? ? ? ? ? ? ? ? rInflate(parser, root, inflaterContext, attrs, false);
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? // 從 xml 中加載布局控件
? ? ? ? ? ? ? ? final View temp = createViewFromTag(root, name, inflaterContext, attrs);
? ? ? ? ? ? ? ? // 生成布局參數 LayoutParams
? ? ? ? ? ? ? ? ViewGroup.LayoutParams params = null;
? ? ? ? ? ? ? ? if (root != null) {
? ? ? ? ? ? ? ? ? ? params = root.generateLayoutParams(attrs);
? ? ? ? ? ? ? ? ? ? if (!attachToRoot) {
? ? ? ? ? ? ? ? ? ? ? ? temp.setLayoutParams(params);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? // 加載子控件
? ? ? ? ? ? ? ? rInflateChildren(parser, temp, attrs, true);
? ? ? ? ? ? ? ? // 添加到根控件
? ? ? ? ? ? ? ? if (root != null && attachToRoot) {
? ? ? ? ? ? ? ? ? ? root.addView(temp, params);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? if (root == null || !attachToRoot) {
? ? ? ? ? ? ? ? ? ? result = temp;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? } catch (XmlPullParserException e) {/*...*/}
? ? ? ? return result;
? ? }
}

接下來,我們看一下createViewFromTag()方法。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
? ? // 老的布局方式
? ? if (name.equals("view")) {
? ? ? ? name = attrs.getAttributeValue(null, "class");
? ? }
? ? // 處理 theme
? ? if (!ignoreThemeAttr) {
? ? ? ? final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
? ? ? ? final int themeResId = ta.getResourceId(0, 0);
? ? ? ? if (themeResId != 0) {
? ? ? ? ? ? context = new ContextThemeWrapper(context, themeResId);
? ? ? ? }
? ? ? ? ta.recycle();
? ? }
? ? try {
? ? ? ? View view = tryCreateView(parent, name, context, attrs);
? ? ? ? if (view == null) {
? ? ? ? ? ? final Object lastContext = mConstructorArgs[0];
? ? ? ? ? ? mConstructorArgs[0] = context;
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? if (-1 == name.indexOf('.')) {
? ? ? ? ? ? ? ? ? ? view = onCreateView(context, parent, name, attrs);
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? view = createView(context, name, null, attrs);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } finally {
? ? ? ? ? ? ? ? mConstructorArgs[0] = lastContext;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return view;
? ? } catch (InflateException e) {
? ? ? ? // ...
? ? }
}
public final View tryCreateView(View parent, String name, Context context, AttributeSet attrs) {
? ? if (name.equals(TAG_1995)) {
? ? ? ? return new BlinkLayout(context, attrs);
? ? }
? ? // 優(yōu)先使用 mFactory2 創(chuàng)建 view,mFactory2 為空則使用 mFactory,否則使用 mPrivateFactory
? ? 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;
? ? }
? ? if (view == null && mPrivateFactory != null) {
? ? ? ? view = mPrivateFactory.onCreateView(parent, name, context, attrs);
? ? }
? ? return view;
}

可以看出,這里優(yōu)先使用 mFactory2 創(chuàng)建 view,mFactory2 為空則使用 mFactory,否則使用 mPrivateFactory 加載 view。所以,如果我們想要對 view 創(chuàng)建過程進行 hook,就應該 hook 這里的 mFactory2,因為它的優(yōu)先級最高。
注意到這里的 方法中并沒有循環(huán),所以,第一次的時候只能加載根布局。那么根布局內的子控件是如何加載的呢?這就用到了inflaterInflateChildren()方法。

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
? ? ? ? boolean finishInflate) throws XmlPullParserException, IOException {
? ? rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
? ? final int depth = parser.getDepth();
? ? int type;
? ? boolean pendingRequestFocus = false;
? ? while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
? ? ? ? if (type != XmlPullParser.START_TAG) continue;
? ? ? ? final String name = parser.getName();
? ? ? ? if (TAG_REQUEST_FOCUS.equals(name)) {
? ? ? ? ? ? // 處理 requestFocus 標簽
? ? ? ? ? ? pendingRequestFocus = true;
? ? ? ? ? ? consumeChildElements(parser);
? ? ? ? } else if (TAG_TAG.equals(name)) {
? ? ? ? ? ? // 處理 tag 標簽
? ? ? ? ? ? parseViewTag(parser, parent, attrs);
? ? ? ? } else if (TAG_INCLUDE.equals(name)) {
? ? ? ? ? ? // 處理 include 標簽
? ? ? ? ? ? if (parser.getDepth() == 0) {
? ? ? ? ? ? ? ? throw new InflateException("<include /> cannot be the root element");
? ? ? ? ? ? }
? ? ? ? ? ? parseInclude(parser, context, parent, attrs);
? ? ? ? } else if (TAG_MERGE.equals(name)) {
? ? ? ? ? ? // 處理 merge 標簽
? ? ? ? ? ? throw new InflateException("<merge /> must be the root element");
? ? ? ? } else {
? ? ? ? ? ? // 這里處理的是普通的 view 標簽
? ? ? ? ? ? final View view = createViewFromTag(parent, name, context, attrs);
? ? ? ? ? ? final ViewGroup viewGroup = (ViewGroup) parent;
? ? ? ? ? ? final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
? ? ? ? ? ? // 繼續(xù)處理子控件
? ? ? ? ? ? rInflateChildren(parser, view, attrs, true);
? ? ? ? ? ? viewGroup.addView(view, params);
? ? ? ? }
? ? }
? ? if (pendingRequestFocus) {
? ? ? ? parent.restoreDefaultFocus();
? ? }
? ? if (finishInflate) {
? ? ? ? parent.onFinishInflate();
? ? }
}

注意到該方法內部又調用了createViewFromTag和rInflateChildren方法,也就是說,這里通過遞歸的方式實現(xiàn)對整個 view 樹的遍歷,從而將整個 xml 加載為 view 樹。以上是安卓的 LayoutInflater 從 xml 中加載控件的邏輯,可以看出我們可以通過 hook 實現(xiàn)對創(chuàng)建 view 的過程的“監(jiān)聽”。
上面我們說了下?lián)Q膚的原理,下面我們介紹幾種Android換膚的技術框架:Android-Skin-Loader、ThemeSkinning和Android-skin-support。

3.2 Android-Skin-Loader

3.2.1 使用流程

學習了 Hook LayoutInflator 的底層原理之后,我們來看幾個基于這種原理實現(xiàn)的換膚方案。首先是 Android-Skin-Loader 這個庫,這個庫需要你覆寫Activity,然后再替換皮膚,Activity部分代碼如下。

public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{
? ? private SkinInflaterFactory mSkinInflaterFactory;
? ? @Override
? ? protected void onCreate(Bundle savedInstanceState) {
? ? ? ? super.onCreate(savedInstanceState);
? ? ? ? mSkinInflaterFactory = new SkinInflaterFactory();
? ? ? ? getLayoutInflater().setFactory(mSkinInflaterFactory);
? ? }
? ? // ...
}

可以看出,這里將自定義的 Factory 設置給了LayoutInflator,SkinInflaterFactory的實現(xiàn)如下:

public class SkinInflaterFactory implements Factory {
? ? private static final boolean DEBUG = true;
? ? private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
? ? @Override
? ? public View onCreateView(String name, Context context, AttributeSet attrs) {
? ? ? ? // 讀取自定義屬性 enable,這里用了自定義的 namespace
? ? ? ? boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
? ? ? ? if (!isSkinEnable){
? ? ? ? ? ? return null;
? ? ? ? }
? ? ? ? // 創(chuàng)建 view
? ? ? ? View view = createView(context, name, attrs);
? ? ? ? if (view == null){
? ? ? ? ? ? return null;
? ? ? ? }
? ? ? ? parseSkinAttr(context, attrs, view);
? ? ? ? return view;
? ? }
? ? private View createView(Context context, String name, AttributeSet attrs) {
? ? ? ? View view = null;
? ? ? ? try {
? ? ? ? ? ? // 兼容低版本創(chuàng)建 view 的邏輯(低版本是沒有完整包名)
? ? ? ? ? ? if (-1 == name.indexOf('.')){
? ? ? ? ? ? ? ? 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 {
? ? ? ? ? ? ? ? // 新的創(chuàng)建 view 的邏輯
? ? ? ? ? ? ? ? view = LayoutInflater.from(context).createView(name, null, attrs);
? ? ? ? ? ? }
? ? ? ? } catch (Exception e) {?
? ? ? ? ? ? view = null;
? ? ? ? }
? ? ? ? return view;
? ? }
? ? private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
? ? ? ? List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
? ? ? ? // 對 xml 中控件的屬性進行解析
? ? ? ? for (int i = 0; i < attrs.getAttributeCount(); i++){
? ? ? ? ? ? String attrName = attrs.getAttributeName(i);
? ? ? ? ? ? String attrValue = attrs.getAttributeValue(i);
? ? ? ? ? ? // 判斷屬性是否支持,屬性是預定義的
? ? ? ? ? ? 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);
? ? ? ? ? ? ? ? ? ? if (mSkinAttr != null) {
? ? ? ? ? ? ? ? ? ? ? ? viewAttrs.add(mSkinAttr);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? } catch (NumberFormatException e) {/*...*/}
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? if(!ListUtils.isEmpty(viewAttrs)){
? ? ? ? ? ? // 構建該控件的屬性關系
? ? ? ? ? ? SkinItem skinItem = new SkinItem();
? ? ? ? ? ? skinItem.view = view;
? ? ? ? ? ? skinItem.attrs = viewAttrs;
? ? ? ? ? ? mSkinItems.add(skinItem);
? ? ? ? ? ? if(SkinManager.getInstance().isExternalSkin()){
? ? ? ? ? ? ? ? skinItem.apply();
? ? ? ? ? ? }
? ? ? ? }
? ? }
}

這里自定義了一個 xml 屬性,用來指定是否啟用換膚配置。然后在創(chuàng)建 view 的過程中解析 xml 中定義的 view 的屬性信息,比如,background 和 textColor 等屬性。并將其對應的屬性、屬性值和控件以映射的形式記錄到緩存中。當發(fā)生換膚的時候根據這里的映射關系在代碼中更新控件的屬性信息。

public class BackgroundAttr extends SkinAttr {
? ? @Override
? ? public void apply(View view) {
? ? ? ? if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
? ? ? ? ? ? // 注意這里獲取屬性值的時候是通過 SkinManager 的方法獲取的
? ? ? ? ? ? view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
? ? ? ? }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
? ? ? ? ? ? Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
? ? ? ? ? ? view.setBackground(bg);
? ? ? ? }
? ? }
}

如果是動態(tài)添加的 view,比如在 java 代碼中,該庫提供了 等方法來動態(tài)添加映射關系到緩存中。在 activity 的生命周期方法中注冊監(jiān)聽換膚事件:

public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{
? ? @Override
? ? protected void onResume() {
? ? ? ? super.onResume();
? ? ? ? SkinManager.getInstance().attach(this);
? ? }
? ? @Override
? ? protected void onDestroy() {
? ? ? ? super.onDestroy();
? ? ? ? SkinManager.getInstance().detach(this);
? ? ? ? // 清理緩存數據
? ? ? ? mSkinInflaterFactory.clean();
? ? }
? ? @Override
? ? public void onThemeUpdate() {
? ? ? ? if(!isResponseOnSkinChanging){
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ? mSkinInflaterFactory.applySkin();
? ? }
? ? // ...?
}

當換膚的時候會通知到 Activity 并觸發(fā)onThemeUpdate方法,接著調用 SkinInflaterFactory 的 apply 方法。SkinInflaterFactory 的 apply 方法中對緩存的屬性信息遍歷更新實現(xiàn)換膚。

3.2.2 皮膚包加載邏輯

接下來,我們看一下皮膚包的加載邏輯,即通過自定義的 AssetManager 實現(xiàn),類似于插件化。

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) {
? ? ? ? ? ? ? ? ? ? String skinPkgPath = params[0];
? ? ? ? ? ? ? ? ? ? File file = new File(skinPkgPath);?
? ? ? ? ? ? ? ? ? ? if(file == null || !file.exists()){
? ? ? ? ? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? 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) { /*...*/ }
? ? ? ? };
? ? ? ? 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);
}

然后,在獲取值的時候使用下面的方法:

public int getColor(int resId){
? ? int originColor = context.getResources().getColor(resId);
? ? if(mResources == null || isDefaultSkin){
? ? ? ? return originColor;
? ? }
? ? 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;
}

3.2.3 方案特點

此種方案換膚,有如下的一些特點:

  • 換膚需要繼承自定義 activity
  • 皮膚包和 APK 如果使用了資源混淆加載的時候就會出現(xiàn)問題
  • 沒處理屬性值通過 的形式引用的情況?attr
  • 每個換膚的屬性需要自己注冊并實現(xiàn)
  • 有些控件的一些屬性可能沒有提供對應的 java 方法,因此在代碼中換膚就行不通
  • 沒有處理使用 style 的情況
  • 基于 實現(xiàn),版本太老android.app.Activity

在 inflator 創(chuàng)建 view 的時候,其實只做了對屬性的攔截處理操作,可以通過代理系統(tǒng)的 Factory 實現(xiàn)創(chuàng)建 view 的操作

3.3 ThemeSkinning

這個庫是基于上面的 Android-Skin-Loader 開發(fā)的,在其基礎之上做了許多的調整,其地址是 ThemeSkinning。主要調整的內容如下:

3.3.1 AppCompactActivity調整

該庫基于 AppCompactActivity 和LayoutInflaterCompat.setFactory開發(fā),改動的內容如下:

public class SkinBaseActivity extends AppCompatActivity implements ISkinUpdate, IDynamicNewView {
? ? private SkinInflaterFactory mSkinInflaterFactory;
? ? private final static String TAG = "SkinBaseActivity";
? ? @Override
? ? protected void onCreate(Bundle savedInstanceState) {
? ? ? ? mSkinInflaterFactory = new SkinInflaterFactory(this);
? ? ? ? LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinInflaterFactory);
? ? ? ? super.onCreate(savedInstanceState);
? ? ? ? changeStatusColor();
? ? }
? ? // ...
}

同時,該庫也提供了修改狀態(tài)欄的方法,雖然能力比較有限。

3.3.2 SkinInflaterFactory調整

SkinInflaterFactory對創(chuàng)建View做了一些調整,代碼如下:

public class SkinInflaterFactory implements LayoutInflater.Factory2 {
? ? private Map<View, SkinItem> mSkinItemMap = new HashMap<>();
? ? private AppCompatActivity mAppCompatActivity;
? ? public SkinInflaterFactory(AppCompatActivity appCompatActivity) {
? ? ? ? this.mAppCompatActivity = appCompatActivity;
? ? }
? ? @Override
? ? public View onCreateView(String s, Context context, AttributeSet attributeSet) {
? ? ? ? return null;
? ? }
? ? @Override
? ? public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
? ? ? ? // 沿用之前的一些邏輯
? ? ? ? boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
? ? ? ? AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
? ? ? ? View view = delegate.createView(parent, name, context, attrs);
? ? ? ? // 對字體兼容做了支持,這里是通過靜態(tài)方式將其緩存到內存,動態(tài)新增和移除,加載字體之后調用 textview 的 settypeface 方法替換
? ? ? ? if (view instanceof TextView && SkinConfig.isCanChangeFont()) {
? ? ? ? ? ? TextViewRepository.add(mAppCompatActivity, (TextView) view);
? ? ? ? }
? ? ? ? if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {
? ? ? ? ? ? if (view == null) {
? ? ? ? ? ? ? ? // 創(chuàng)建 view 的邏輯做了調整
? ? ? ? ? ? ? ? view = ViewProducer.createViewFromTag(context, name, attrs);
? ? ? ? ? ? }
? ? ? ? ? ? if (view == null) {
? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? }
? ? ? ? ? ? parseSkinAttr(context, attrs, view);
? ? ? ? }
? ? ? ? return view;
? ? }
? ? // ...
}

以下是View的創(chuàng)建邏輯的相關代碼:

class ViewProducer {
? ? private static final Object[] mConstructorArgs = new Object[2];
? ? private static final Map<String, Constructor<? extends View>> sConstructorMap = new ArrayMap<>();
? ? private static final Class<?>[] sConstructorSignature = new Class[]{Context.class, AttributeSet.class};
? ? private static final String[] sClassPrefixList = {"android.widget.", "android.view.", "android.webkit."};
? ? static View createViewFromTag(Context context, String name, AttributeSet attrs) {
? ? ? ? if (name.equals("view")) {
? ? ? ? ? ? name = attrs.getAttributeValue(null, "class");
? ? ? ? }
? ? ? ? try {
? ? ? ? ? ? // 構造參數,緩存,復用
? ? ? ? ? ? mConstructorArgs[0] = context;
? ? ? ? ? ? mConstructorArgs[1] = attrs;
? ? ? ? ? ? if (-1 == name.indexOf('.')) {
? ? ? ? ? ? ? ? for (int i = 0; i < sClassPrefixList.length; i++) {
? ? ? ? ? ? ? ? ? ? final View view = createView(context, name, sClassPrefixList[i]);
? ? ? ? ? ? ? ? ? ? if (view != null) {
? ? ? ? ? ? ? ? ? ? ? ? return view;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? // 通過構造方法創(chuàng)建 view
? ? ? ? ? ? ? ? return createView(context, name, null);
? ? ? ? ? ? }
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? return null;
? ? ? ? } finally {
? ? ? ? ? ? mConstructorArgs[0] = null;
? ? ? ? ? ? mConstructorArgs[1] = null;
? ? ? ? }
? ? }
? ? // ...
}

3.3.3 對style的兼容處理

private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
? ? List<SkinAttr> viewAttrs = new ArrayList<>();
? ? for (int i = 0; i < attrs.getAttributeCount(); i++) {
? ? ? ? String attrName = attrs.getAttributeName(i);
? ? ? ? String attrValue = attrs.getAttributeValue(i);
? ? ? ? if ("style".equals(attrName)) {
? ? ? ? ? ? // 對 style 的處理,從 theme 中獲取 TypedArray 然后獲取 resource id,再獲取對應的信息
? ? ? ? ? ? int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background};
? ? ? ? ? ? TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0);
? ? ? ? ? ? int textColorId = a.getResourceId(0, -1);
? ? ? ? ? ? int backgroundId = a.getResourceId(1, -1);
? ? ? ? ? ? if (textColorId != -1) {
? ? ? ? ? ? ? ? String entryName = context.getResources().getResourceEntryName(textColorId);
? ? ? ? ? ? ? ? String typeName = context.getResources().getResourceTypeName(textColorId);
? ? ? ? ? ? ? ? SkinAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName);
? ? ? ? ? ? ? ? if (skinAttr != null) {
? ? ? ? ? ? ? ? ? ? viewAttrs.add(skinAttr);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? if (backgroundId != -1) {
? ? ? ? ? ? ? ? String entryName = context.getResources().getResourceEntryName(backgroundId);
? ? ? ? ? ? ? ? String typeName = context.getResources().getResourceTypeName(backgroundId);
? ? ? ? ? ? ? ? SkinAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName);
? ? ? ? ? ? ? ? if (skinAttr != null) {
? ? ? ? ? ? ? ? ? ? viewAttrs.add(skinAttr);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? a.recycle();
? ? ? ? ? ? continue;
? ? ? ? }
? ? ? ? if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {
? ? ? ? ? ? // 老邏輯
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? //resource id
? ? ? ? ? ? ? ? int id = Integer.parseInt(attrValue.substring(1));
? ? ? ? ? ? ? ? if (id == 0) continue;
? ? ? ? ? ? ? ? String entryName = context.getResources().getResourceEntryName(id);
? ? ? ? ? ? ? ? String typeName = context.getResources().getResourceTypeName(id);
? ? ? ? ? ? ? ? SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
? ? ? ? ? ? ? ? if (mSkinAttr != null) {
? ? ? ? ? ? ? ? ? ? viewAttrs.add(mSkinAttr);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } catch (NumberFormatException e) { /*...*/ }
? ? ? ? }
? ? }
? ? if (!SkinListUtils.isEmpty(viewAttrs)) {
? ? ? ? SkinItem skinItem = new SkinItem();
? ? ? ? skinItem.view = view;
? ? ? ? skinItem.attrs = viewAttrs;
? ? ? ? mSkinItemMap.put(skinItem.view, skinItem);
? ? ? ? if (SkinManager.getInstance().isExternalSkin() ||
? ? ? ? ? ? ? ? SkinManager.getInstance().isNightMode()) {//如果當前皮膚來自于外部或者是處于夜間模式
? ? ? ? ? ? skinItem.apply();
? ? ? ? }
? ? }
}

3.3.4 fragment 調整

在 Fragment 的生命周期方法結束的時候從緩存當中移除指定的 View。

@Override
public void onDestroyView() {
? ? removeAllView(getView());
? ? super.onDestroyView();
}
protected void removeAllView(View v) {
? ? if (v instanceof ViewGroup) {
? ? ? ? ViewGroup viewGroup = (ViewGroup) v;
? ? ? ? for (int i = 0; i &lt; viewGroup.getChildCount(); i++) {
? ? ? ? ? ? removeAllView(viewGroup.getChildAt(i));
? ? ? ? }
? ? ? ? removeViewInSkinInflaterFactory(v);
? ? } else {
? ? ? ? removeViewInSkinInflaterFactory(v);
? ? }
}

這種方案相對第一個框架改進了很多,但是此庫已經有4,5年沒有維護了,組件和代碼都比較老。

3.4 Android-skin-support

接下來,我們再看一下Android-skin-support 。主要修改的部分如下:

3.4.1 自動注冊 layoutinflator.factory

public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
? ? private SkinActivityLifecycle(Application application) {
? ? ? ? application.registerActivityLifecycleCallbacks(this);
? ? ? ? installLayoutFactory(application);
? ? ? ? // 注冊監(jiān)聽
? ? ? ? SkinCompatManager.getInstance().addObserver(getObserver(application));
? ? }
? ? @Override
? ? public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
? ? ? ? if (isContextSkinEnable(activity)) {
? ? ? ? ? ? installLayoutFactory(activity);
? ? ? ? ? ? // 更新 acitvity 的窗口的背景
? ? ? ? ? ? updateWindowBackground(activity);
? ? ? ? ? ? // 觸發(fā)換膚...如果 view 沒有創(chuàng)建是不是就容易導致 NPE?
? ? ? ? ? ? if (activity instanceof SkinCompatSupportable) {
? ? ? ? ? ? ? ? ((SkinCompatSupportable) activity).applySkin();
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? private void installLayoutFactory(Context context) {
? ? ? ? try {
? ? ? ? ? ? LayoutInflater layoutInflater = LayoutInflater.from(context);
? ? ? ? ? ? LayoutInflaterCompat.setFactory2(layoutInflater, getSkinDelegate(context));
? ? ? ? } catch (Throwable e) { /* ... */ }
? ? }
? ? // 獲取 LayoutInflater.Factory2,這里加了一層緩存
? ? private SkinCompatDelegate getSkinDelegate(Context context) {
? ? ? ? if (mSkinDelegateMap == null) {
? ? ? ? ? ? mSkinDelegateMap = new WeakHashMap<>();
? ? ? ? }
? ? ? ? SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
? ? ? ? if (mSkinDelegate == null) {
? ? ? ? ? ? mSkinDelegate = SkinCompatDelegate.create(context);
? ? ? ? ? ? mSkinDelegateMap.put(context, mSkinDelegate);
? ? ? ? }
? ? ? ? return mSkinDelegate;
? ? }
? ? // ...
}

LayoutInflaterCompat.setFactory2()方法源碼如下:

public final class LayoutInflaterCompat {
? ? public static void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
? ? ? ? inflater.setFactory2(factory);
? ? ? ? if (Build.VERSION.SDK_INT < 21) {
? ? ? ? ? ? final LayoutInflater.Factory f = inflater.getFactory();
? ? ? ? ? ? if (f instanceof LayoutInflater.Factory2) {
? ? ? ? ? ? ? ? forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? forceSetFactory2(inflater, factory);
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? // 通過反射的方式直接修改 mFactory2 字段
? ? private static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
? ? ? ? if (!sCheckedField) {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
? ? ? ? ? ? ? ? sLayoutInflaterFactory2Field.setAccessible(true);
? ? ? ? ? ? } catch (NoSuchFieldException e) { /* ... */ }
? ? ? ? ? ? sCheckedField = true;
? ? ? ? }
? ? ? ? if (sLayoutInflaterFactory2Field != null) {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? sLayoutInflaterFactory2Field.set(inflater, factory);
? ? ? ? ? ? } catch (IllegalAccessException e) { /* ... */ }
? ? ? ? }
? ? }
? ? // ...
}

3.4.2 LayoutInflater.Factory2

public class SkinCompatDelegate implements LayoutInflater.Factory2 {
? ? @Override
? ? public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
? ? ? ? View view = createView(parent, name, context, attrs);
? ? ? ? if (view == null) return null;
? ? ? ? // 加入緩存
? ? ? ? if (view instanceof SkinCompatSupportable) {
? ? ? ? ? ? mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
? ? ? ? }
? ? ? ? return view;
? ? }
? ? @Override
? ? public View onCreateView(String name, Context context, AttributeSet attrs) {
? ? ? ? View view = createView(null, name, context, attrs);
? ? ? ? if (view == null) return null;
? ? ? ? // 加入緩存,繼承這個接口的主要是 view 和 activity 這些
? ? ? ? if (view instanceof SkinCompatSupportable) {
? ? ? ? ? ? mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
? ? ? ? }
? ? ? ? return view;
? ? }
? ? public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
? ? ? ? // view 生成邏輯被包裝成了 SkinCompatViewInflater
? ? ? ? if (mSkinCompatViewInflater == null) {
? ? ? ? ? ? mSkinCompatViewInflater = new SkinCompatViewInflater();
? ? ? ? }
? ? ? ? List<SkinWrapper> wrapperList = SkinCompatManager.getInstance().getWrappers();
? ? ? ? for (SkinWrapper wrapper : wrapperList) {
? ? ? ? ? ? Context wrappedContext = wrapper.wrapContext(mContext, parent, attrs);
? ? ? ? ? ? if (wrappedContext != null) {
? ? ? ? ? ? ? ? context = wrappedContext;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? //?
? ? ? ? return mSkinCompatViewInflater.createView(parent, name, context, attrs);
? ? }
? ? // ...
}

3.4.3 SkinCompatViewInflater

上述方法中 SkinCompatViewInflater 獲取 view 的邏輯如下。

public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
? ? // 通過 inflator 創(chuàng)建 view
? ? View view = createViewFromHackInflater(context, name, attrs);
? ? if (view == null) {
? ? ? ? view = createViewFromInflater(context, name, attrs);
? ? }
? ? // 根據 view 標簽創(chuàng)建 view
? ? if (view == null) {
? ? ? ? view = createViewFromTag(context, name, attrs);
? ? }
? ? // 處理 xml 中設置的點擊事件
? ? if (view != null) {
? ? ? ? checkOnClickListener(view, attrs);
? ? }
? ? return view;
}
private View createViewFromHackInflater(Context context, String name, AttributeSet attrs) {
? ? View view = null;
? ? for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getHookInflaters()) {
? ? ? ? view = inflater.createView(context, name, attrs);
? ? ? ? if (view == null) {
? ? ? ? ? ? continue;
? ? ? ? } else {
? ? ? ? ? ? break;
? ? ? ? }
? ? }
? ? return view;
}
private View createViewFromInflater(Context context, String name, AttributeSet attrs) {
? ? View view = null;
? ? for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {
? ? ? ? view = inflater.createView(context, name, attrs);
? ? ? ? if (view == null) {
? ? ? ? ? ? continue;
? ? ? ? } else {
? ? ? ? ? ? break;
? ? ? ? }
? ? }
? ? return view;
}
public View createViewFromTag(Context context, String name, AttributeSet attrs) {
? ? // <view class="xxxx"> 形式的 tag,和 <xxxx> 一樣
? ? if ("view".equals(name)) {
? ? ? ? name = attrs.getAttributeValue(null, "class");
? ? }
? ? try {
? ? ? ? // 構造參數緩存
? ? ? ? mConstructorArgs[0] = context;
? ? ? ? mConstructorArgs[1] = attrs;
? ? ? ? if (-1 == name.indexOf('.')) {
? ? ? ? ? ? for (int i = 0; i < sClassPrefixList.length; i++) {
? ? ? ? ? ? ? ? // 通過構造方法創(chuàng)建 view
? ? ? ? ? ? ? ? final View view = createView(context, name, sClassPrefixList[i]);
? ? ? ? ? ? ? ? if (view != null) {
? ? ? ? ? ? ? ? ? ? return view;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? return null;
? ? ? ? } else {
? ? ? ? ? ? return createView(context, name, null);
? ? ? ? }
? ? } catch (Exception e) {
? ? ? ? return null;
? ? } finally {
? ? ? ? mConstructorArgs[0] = null;
? ? ? ? mConstructorArgs[1] = null;
? ? }
}
這里用來創(chuàng)建視圖 的充氣器 是通過 獲取的。這樣設計的目的在于暴露接口給調用者,用來自定義控件的充氣器 邏輯。比如,針對三方控件和自定義控件的邏輯等。SkinCompatManager.getInstance().getInflaters()
該庫自帶的一個實現(xiàn)是,
public class SkinAppCompatViewInflater implements SkinLayoutInflater, SkinWrapper {
? ?@Override
? ? public View createView(Context context, String name, AttributeSet attrs) {
? ? ? ? View view = createViewFromFV(context, name, attrs);
? ? ? ? if (view == null) {
? ? ? ? ? ? view = createViewFromV7(context, name, attrs);
? ? ? ? }
? ? ? ? return view;
? ? }
? ? private View createViewFromFV(Context context, String name, AttributeSet attrs) {
? ? ? ? View view = null;
? ? ? ? if (name.contains(".")) {
? ? ? ? ? ? return null;
? ? ? ? }
? ? ? ? switch (name) {
? ? ? ? ? ? case "View":
? ? ? ? ? ? ? ? view = new SkinCompatView(context, attrs);
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? case "LinearLayout":
? ? ? ? ? ? ? ? view = new SkinCompatLinearLayout(context, attrs);
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? // ... 其他控件的實現(xiàn)邏輯
? ? ? ? }
? ? }
? ? // ...
}

四、其他方案

除了上面介紹的方案外,還有如下的一些方案:

4.1 TG換膚方案

TG 的換膚只支持夜間和日間主題之間的切換,所以,相對上面幾種方案 TG 的換膚就簡單得多。

在閱讀 TG 的代碼的時候,我也 TG 在做頁面布局的時候做了一件很瘋狂的事情——他們沒有使用任何 xml 布局,所有布局都是通過 java 代碼實現(xiàn)的。

為了支持對主題的自定義 TG 把項目內幾乎所有的顏色分別定義了一個名稱,對以文本形式記錄到一個文件中,數量非常多,然后將其放到 assets 下面,應用內通過讀取這個資源文件來獲取各個控件的顏色。

4.2 自定義控件 + 全局廣播實現(xiàn)換膚

這種方案根前面 hook LayoutInflator 的自動替換視圖 的方案差不多。不過,這種方案不需要做 hook,而是對應用的內常用的控件全部做一邊自定義。自定義控件內部監(jiān)聽換膚的事件。

當自定義控件接收到換膚事件的時候,自定義控件內部觸發(fā)換膚邏輯。不過這種換膚的方案相對于上述通過 hook LayoutInflator 的方案而言,可控性更好一些。

以上就是Android 換膚指南的詳細內容,更多關于Android 換膚指南的資料請關注腳本之家其它相關文章!

相關文章

  • 獲取Android簽名證書的公鑰和私鑰的簡單實例

    獲取Android簽名證書的公鑰和私鑰的簡單實例

    下面小編就為大家?guī)硪黄@取Android簽名證書的公鑰和私鑰的簡單實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-12-12
  • flutter實現(xiàn)倒計時加載頁面

    flutter實現(xiàn)倒計時加載頁面

    這篇文章主要為大家詳細介紹了flutter實現(xiàn)倒計時加載頁面,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • Android中使用Kotlin實現(xiàn)一個簡單的登錄界面

    Android中使用Kotlin實現(xiàn)一個簡單的登錄界面

    Kotlin 是一種在 Java 虛擬機上運行的靜態(tài)類型編程語言,被稱之為 Android 世界的Swift,由 JetBrains 設計開發(fā)并開源。接下來本文通過實例代碼給大家講解Android中使用Kotlin實現(xiàn)一個簡單的登錄界面,一起看看吧
    2017-09-09
  • Android開發(fā)之線程通信詳解

    Android開發(fā)之線程通信詳解

    這篇文章主要為大家詳細介紹了Android開發(fā)中線程間通信的相關資料,文中的示例代碼講解詳細,對我們學習Android有一定的幫助,?需要的可以了解一下
    2022-11-11
  • Android自定義view實現(xiàn)電影票在線選座功能

    Android自定義view實現(xiàn)電影票在線選座功能

    這篇文章主要為大家詳細介紹了Android自定義view實現(xiàn)選座功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-11-11
  • Android Studio 當build時候出錯解決辦法

    Android Studio 當build時候出錯解決辦法

    這篇文章主要介紹了 Android Studio在build的時候出現(xiàn)transformClassesWithDexForDebug錯誤解決辦法的相關資料,需要的朋友可以參考下
    2017-05-05
  • Android ExpandableListView使用方法案例詳解

    Android ExpandableListView使用方法案例詳解

    這篇文章主要介紹了Android ExpandableListView使用方法案例詳解,本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下
    2021-08-08
  • Android 6.0開發(fā)實現(xiàn)關機菜單添加重啟按鈕的方法

    Android 6.0開發(fā)實現(xiàn)關機菜單添加重啟按鈕的方法

    這篇文章主要介紹了Android 6.0開發(fā)實現(xiàn)關機菜單添加重啟按鈕的方法,涉及Android6.0針對相關源碼的修改與功能添加操作技巧,需要的朋友可以參考下
    2017-09-09
  • Android編程輸入事件流程詳解

    Android編程輸入事件流程詳解

    這篇文章主要介紹了Android編程輸入事件流程,較為詳細的分析了Android輸入事件原理、相關概念與具體操作流程,需要的朋友可以參考下
    2016-10-10
  • Android實現(xiàn)滑動屏幕切換圖片

    Android實現(xiàn)滑動屏幕切換圖片

    這篇文章主要為大家詳細介紹了Android實現(xiàn)滑動屏幕切換圖片,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-08-08

最新評論