編輯:關於Android編程
在上一篇Android小結博客中我們簡要的介紹了常見的換膚手法,換膚可以貫穿在整個Android開發中,所以必然可以做成一個框架集的形式。下面我們就來看看一些Android換膚開源組件。
Colorful是一個基於Theme實現的開源控件,作者是mr_simple。Colorful無需重啟Activity、無需自定義View,方便的實現日間、夜間模式。在上篇文章中,我們介紹了基於Theme的切換都需要重新OnCreate Activity。但是Colorful卻不需要重啟Activity,下面就讓我們一睹風采。
基於Theme的換膚,就需要我們定義好我們的屬性和Theme屬性,這些都是必備的。使用Colorful:
Colorful mColorful; // 構建Colorful對象來綁定View與屬性的對象關系 mColorful = new Colorful.Builder(this) .backgroundDrawable(R.id.root_view, R.attr.root_view_bg) // 設置view的背景圖片 .backgroundColor(R.id.change_btn, R.attr.btn_bg) // 設置背景色 .textColor(R.id.textview, R.attr.text_color) .setter(listViewSetter) // 手動設置setter .create(); // 設置文本顏色 mColorful.setTheme(R.style.NightTheme)
這就是簡單的使用。
我們查看下Colorful這個控制類:
public final class Colorful { /** * Colorful Builder */ Builder mBuilder; /** * private constructor * * @param builder */ private Colorful(Builder builder) { mBuilder = builder; } /** * 設置新的主題 * * @param newTheme */ public void setTheme(int newTheme) { mBuilder.setTheme(newTheme); } /** * * 構建Colorful的Builder對象 * * @author mrsimple * */ public static class Builder { /** * 存儲了視圖和屬性資源id的關系表 */ SetmElements = new HashSet (); /** * 目標Activity */ Activity mActivity; /** * @param activity */ public Builder(Activity activity) { mActivity = activity; } /** * @param fragment */ public Builder(Fragment fragment) { mActivity = fragment.getActivity(); } private View findViewById(int viewId) { return mActivity.findViewById(viewId); } /** * 將View id與存儲該view背景色的屬性進行綁定 * @param viewId * 控件id * @param colorId * 顏色屬性id * @return */ public Builder backgroundColor(int viewId, int colorId) { mElements.add(new ViewBackgroundColorSetter(findViewById(viewId), colorId)); return this; } /** * 將View id與存儲該view背景Drawable的屬性進行綁定 * @param viewId * 控件id * @param colorId * Drawable屬性id * @return */ public Builder backgroundDrawable(int viewId, int drawableId) { mElements.add(new ViewBackgroundDrawableSetter( findViewById(viewId), drawableId)); return this; } /** * 將TextView id與存儲該TextView文本顏色的屬性進行綁定 * @param viewId * TextView或者TextView子類控件的id * @param colorId * 顏色屬性id * @return */ public Builder textColor(int viewId, int colorId) { TextView textView = (TextView) findViewById(viewId); mElements.add(new TextColorSetter(textView, colorId)); return this; } /** * 用戶手動構造並且添加Setter * @param setter * 用戶自定義的Setter * @return */ public Builder setter(ViewSetter setter) { mElements.add(setter); return this; } /** * 設置新的主題 * @param newTheme */ protected void setTheme(int newTheme) { mActivity.setTheme(newTheme); makeChange(newTheme); } /** * 修改各個視圖綁定的屬性 */ private void makeChange(int themeId) { Theme curTheme = mActivity.getTheme(); for (ViewSetter setter : mElements) { setter.setValue(curTheme, themeId); } } /** * 創建Colorful對象 * @return */ public Colorful create() { return new Colorful(this); } } }
(1)、從Color的結構定義中可以看到,Colorful的構造函數是一個私有的構造函數,即作者不希望我們直接創建一個Colorful的對象,這裡作者引入一個staitc Builder類,用於構造控件和屬性的對應關系。
(2)、Builder類源碼結構解析: 在Builder源碼類中,我們可以看到裡面包含一個
/** * 存儲了視圖和屬性資源id的關系表 */ SetmElements = new HashSet ();
這就是用於存儲我們的控件和屬性值,然後通過
通過這三個方法綁定控件的id和控件綁定的自定義屬性id,然後在方法的內部,構建ViewSetter對象存儲到Set集合。
當我們建立好這種控件和屬性的對應關系,調用setTheme(id)進行Theme的主題設置,然後該方法調用
/** * 修改各個視圖綁定的屬性 */ private void makeChange(int themeId) { Theme curTheme = mActivity.getTheme(); for (ViewSetter setter : mElements) { setter.setValue(curTheme, themeId); } }
我們可以看到該方法裡面是遍歷Set集合,然後調用ViewSetter的setValue設置值進行修改。總體的思路設計就是這樣。
(3)、上面我們看到了ViewSetter類。該類封裝了我們的控件id和屬性id。
/** * ViewSetter,用於通過{@see #mAttrResId} * 設置View的某個屬性值,例如背景Drawable、背景色、文本顏色等。如需修改其他屬性,可以自行擴展ViewSetter. * * @author mrsimple * */ public abstract class ViewSetter { /** * 目標View */ protected View mView; /** * 目標view id,有時在初始化時還未構建該視圖,比如ListView的Item View中的某個控件 */ protected int mViewId; /** * 目標View要的特定屬性id */ protected int mAttrResId; public ViewSetter(View targetView, int resId) { mView = targetView; mAttrResId = resId; } public ViewSetter(int viewId, int resId) { mViewId = viewId; mAttrResId = resId; } /** * * @param newTheme * @param themeId */ public abstract void setValue(Theme newTheme, int themeId); /** * 獲取視圖的Id * * @return */ protected int getViewId() { return mView != null ? mView.getId() : -1; } protected boolean isViewNotFound() { return mView == null; } /** * * @param newTheme * @param resId * @return */ protected int getColor(Theme newTheme) { TypedValue typedValue = new TypedValue(); newTheme.resolveAttribute(mAttrResId, typedValue, true); return typedValue.data; } }
這裡主要就是看getColor方法,通過resolveAttibute方法獲取Theme裡面的屬性值,然後設置到具體的控件上。這樣就直接設置上了,實現了換膚,並且不需要重啟Activity。比如最直接的例子,TextColorSetter:
public class TextColorSetter extends ViewSetter { public TextColorSetter(TextView textView, int resId) { super(textView, resId); } public TextColorSetter(int viewId, int resId) { super(viewId, resId); } @Override public void setValue(Theme newTheme, int themeId) { if (mView == null) { return; } ((TextView) mView).setTextColor(getColor(newTheme)); } }
這裡的setValue方法就是直接設置值。
總體來說,Colorful基於Theme依然沒有避免掉Theme的弊病,原理是將Thme中的屬性值獲取出來,設置到對應的控件上。而且現在只有setColor方法,即只支持獲取Color的屬性值設置,其他的圖片之類的還沒有豐富。總體功能還不是很完善。
Android-Skin-Loader是一個比較優秀的換膚框架,地址android-sgik-loader,該工程的核心
Android-Skin-Loader ├── android-skin-loader-lib // 皮膚加載庫 ├── android-skin-loader-sample // 皮膚庫應用實例 ├── android-skin-loader-skin // 皮膚包生成demo └── skin-package // 皮膚包輸出目錄
通過這個框架可以對控件進行隨意換膚。
在這個框架中,使用了自定義的Factory對象來完成換膚的操作,這點熟悉LayoutInflater的都知道,這個用於初始化我們的View,具體的我們可以參照Android 探究 LayoutInflater setFactory文章學習下。 換膚需要解決的核心問題有兩個: (1)、外部資源的加載 (2)、定位到需要換膚的View 第一個資源加載的問題可以通過構造AssetManager,反射調用其addAssetPath就可以完成。
第二個問題,就可以利用在onCreateView中,根據view的屬性來定位,例如你可以讓需要換膚的view添加一個自定義的屬性skin_enabled=true(最開始有打印屬性),並且利用一些手段拿到構造到的view,就能在View構造階段定位的需要換膚的View。
在Android-Skin-Loader中,你需要集成封裝的BaseActivity或BaseFragment來進行實現,我們看一下BaseActivity的源碼。
/** * Base Activity for development * * NOTICE: * You should extends from this if you what to do skin change * * @author fengjun */ public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{ /** * Whether response to skin changing after create */ private boolean isResponseOnSkinChanging = true; private SkinInflaterFactory mSkinInflaterFactory; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSkinInflaterFactory = new SkinInflaterFactory(); getLayoutInflater().setFactory(mSkinInflaterFactory); } @Override protected void onResume() { super.onResume(); SkinManager.getInstance().attach(this); } @Override protected void onDestroy() { super.onDestroy(); SkinManager.getInstance().detach(this); mSkinInflaterFactory.clean(); } /** * dynamic add a skin view * * @param view * @param attrName * @param attrValueResId */ protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){ mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId); } protected void dynamicAddSkinEnableView(View view, List pDAttrs){ mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs); } final protected void enableResponseOnSkinChanging(boolean enable){ isResponseOnSkinChanging = enable; } @Override public void onThemeUpdate() { if(!isResponseOnSkinChanging){ return; } mSkinInflaterFactory.applySkin(); } @Override public void dynamicAddView(View view, List pDAttrs) { mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs); } }
通過源碼,我們可以知道,我們需要換膚的界面需要集成BaseActivity才行。在BaseActivity中封裝了自定義的SkinInflaterFactory對象,同時實現了ISkinUpdate、IDynamicNewView兩個接口,用於回調換膚的過程以及添加換膚的View。在BaseActivity中封裝了dynamicAddSkinEnableView、enableResponseOnSkinChanging兩個方法,用於添加添加換膚的View以及設置是否可用換膚。方法實現就是通過SkinInflaterFactory中封裝的方法完成需要換膚View的封裝,然後實現換膚。
在Android-Skin-Loader中的核心類就是SkinInflaterFactory類,該類定義了實現換膚的方法。下面就看看該類的源碼實現:
/** * Supply {@link SkinInflaterFactory} to be called when inflating from a LayoutInflater. * * Use this to collect the {skin:enable="true|false"} views availabled in our XML layout files. * * @author fengjun */ public class SkinInflaterFactory implements Factory { private static final boolean DEBUG = true; /** * Store the view item that need skin changing in the activity */ private List mSkinItems = new ArrayList(); @Override public View onCreateView(String name, Context context, AttributeSet attrs) { // if this is NOT enable to be skined , simplly skip it 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; } /** * Invoke low-level function for instantiating a view by name. This attempts to * instantiate a view class of the given name found in this * LayoutInflater's ClassLoader. * * @param context * @param name The full name of the class to be instantiated. * @param attrs The XML attributes supplied for this instance. * * @return View The newly instantiated view, or null. */ private View createView(Context context, String name, AttributeSet attrs) { View view = null; try { 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 { 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; } /** * Collect skin able tag such as background , textColor and so on * * @param context * @param attrs * @param view */ private void parseSkinAttr(Context context, AttributeSet attrs, View view) { List viewAttrs = new ArrayList(); 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) { 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()){ skinItem.apply(); } } } public void applySkin(){ if(ListUtils.isEmpty(mSkinItems)){ return; } for(SkinItem si : mSkinItems){ if(si.view == null){ continue; } si.apply(); } } public void dynamicAddSkinEnableView(Context context, View view, List pDAttrs){ List viewAttrs = new ArrayList(); SkinItem skinItem = new SkinItem(); skinItem.view = view; for(DynamicAttr dAttr : pDAttrs){ int id = dAttr.refResId; String entryName = context.getResources().getResourceEntryName(id); String typeName = context.getResources().getResourceTypeName(id); SkinAttr mSkinAttr = AttrFactory.get(dAttr.attrName, id, entryName, typeName); viewAttrs.add(mSkinAttr); } skinItem.attrs = viewAttrs; addSkinView(skinItem); } public void dynamicAddSkinEnableView(Context context, View view, String attrName, int attrValueResId){ int id = attrValueResId; String entryName = context.getResources().getResourceEntryName(id); String typeName = context.getResources().getResourceTypeName(id); SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName); SkinItem skinItem = new SkinItem(); skinItem.view = view; List viewAttrs = new ArrayList(); viewAttrs.add(mSkinAttr); skinItem.attrs = viewAttrs; addSkinView(skinItem); } public void addSkinView(SkinItem item){ mSkinItems.add(item); } public void clean(){ if(ListUtils.isEmpty(mSkinItems)){ return; } for(SkinItem si : mSkinItems){ if(si.view == null){ continue; } si.clean(); } } }
通過自定義的Factory實現自定義的View的解析,通過自定義屬性skin:enable="true|false"來進行設置控件是否能夠進行換膚。我們可以看到onCreateView的實現。
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
在這裡通過AttributeSet來獲取控件的自定義屬性(skin:enable)的值。如果isSkinEnable為false,即不是換膚標識的View,直接return null。如果是標識位換膚的控件,則通過createView()方法創建View。然後根據name的類型進行判斷創建View的種類。最後通過parseSkinAttr()方法遍歷View的屬性進行搜集。在這裡補充下一個屬性:private List< SkinItem> mSkinItems,這個成員變量用於記錄存儲的View。這裡使用了SkinItem對象,該對象用於封裝View以及對應的屬性。
public class SkinItem { public View view; public Listattrs; public SkinItem(){ attrs = new ArrayList (); } public void apply(){ if(ListUtils.isEmpty(attrs)){ return; } for(SkinAttr at : attrs){ at.apply(view); } } public void clean(){ if(ListUtils.isEmpty(attrs)){ return; } for(SkinAttr at : attrs){ at = null; } } @Override public String toString() { return "SkinItem [view=" + view.getClass().getSimpleName() + ", attrs=" + attrs + "]"; } }
這裡SkinItem封裝了View以及對應的attrs屬性,同時定義了apply()、clean()兩個方法,用於啟動和清除。然後通過parseSkinAttr()方法遍歷View的屬性,通過SkinItem進行存儲。在SkinInflaterFactory類中,通過dynamicAddSkinEnableView、addSkinView來添加View視圖。然後通過調用applySkin()方法進行換膚。
public void applySkin(){ if(ListUtils.isEmpty(mSkinItems)){ return; } for(SkinItem si : mSkinItems){ if(si.view == null){ continue; } si.apply(); } }
在applySkin()中通過遍歷SkinItem來完成換膚,skinItem.apply方法的本質又是什麼呢?
public void apply(){ if(ListUtils.isEmpty(attrs)){ return; } for(SkinAttr at : attrs){ at.apply(view); } }
在這個方法中又出現了一個SkinAttr類,然後本質就是針對屬性進行設置。在這裡,有BackgroundAttr、DividerAttr等幾個子類,進行實現。
總體來說,Android-Skin-Loader就是通過自定義Factory來實現換膚的操作。
通過上面的例子,我們了解了整體的換膚實現方式,主要就是采用資源的實現方式。以及針對View進行換膚操作的設置。
---- The mark of the immature man is that he wants to die nobly for a causer wh
下面是配置Android開發ADB環境變量的操作步驟。工具/原料win7系統電腦+Android SDK方法/步驟1.首先右擊計算機——屬性——高級系統設置——環境變量;
PullToRefresh是一套實現非常好的下拉刷新庫,它支持:1.ListView2.ExpandableListView3.GridView4.WebView等多種常
本文實例講述了Android編程重寫ViewGroup實現卡片布局的方法。分享給大家供大家參考,具體如下:實現效果如圖:實現思路1. 重寫onMeasure(int wi