Android 動態佈局實現多主題切換
之前做過一個專案(隨心桌布),主要展示過去每期的桌布主題以及相應的桌布,而且策劃要求,最好可以動態變換主題呈現方式,這樣使用者體驗會比較好。嗯,好吧,策劃的話,我們們也沒法反駁,畢竟這樣搞,確實很不錯。於是開始去研究這方面的東西。
首先,我想到的是照片牆效果,改變圖片就能有不同的呈現方式。可是這樣的話,文字以及更深層的自定義效果,就無法實現了。然後,思考了下,決定仿照android原生布局檔案解析方式,自己去動態解析佈局。
先來看下android 原生布局檔案解析流程:
第一步:呼叫LayoutInflater的inflate函式解析xml檔案得到一個view,然後來看看inflate函式:
//使用常見的API方法去解析xml佈局檔案, LayoutInflater layoutInflater = (LayoutInflater)getSystemService(); View root = layoutInflater.inflate(R.layout.main, null,false);
第二步:在inflate函式中,獲取一個XmlResourceParser來解析xml佈局檔案,再往下跟inflate(parser, root, attachToRoot):
public View inflate(int resource, ViewGroup root, boolean attachToRoot) { if (DEBUG) System.out.println("INFLATING from resource: " + resource); XmlResourceParser parser = getContext().getResources().getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
第三步:inflate函式中會根據佈局的節點名建立根檢視,接著根據方法中傳進來的root引數,判斷是否為空,如果不為null,則為該根檢視賦予外面父檢視的佈局引數。接著呼叫rInflate函式來為根檢視新增所有位元組點。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)mConstructorArgs[0]; mConstructorArgs[0] = mContext; //該mConstructorArgs屬性最後會作為引數傳遞給View的建構函式 View result = root; try { // Look for the root node. int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } final String name = parser.getName(); //節點名,即API中的控制元件或者自定義View完整限定名 if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, attrs, false); } else { // Temp is the root view that was found in the xml View temp; if (TAG_1995.equals(name)) { temp = new BlinkLayout(mContext, attrs); } else { temp = createViewFromTag(root, name, attrs); } ViewGroup.LayoutParams params = null; if (root != null) { // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } // Inflate all children under temp rInflate(parser, temp, attrs, true); // We are supposed to attach all the views we found (int temp) // to root. Do that now. if (root != null && attachToRoot) { root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml. if (root == null || !attachToRoot) { result = temp; } } } catch (XmlPullParserException e) { //... } finally { // Don't retain static reference on context. mConstructorArgs[0] = lastContext; mConstructorArgs[1] = null; } return result; } }
第四步:rInflate方法中主要是去遞迴呼叫佈局檔案根檢視的子節點。將解析得到的view新增到parentView。
/** * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). */ void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; 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 />標籤 parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) { //處理<include />標籤 if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } parseInclude(parser, parent, attrs); //解析<include />節點 } else if (TAG_MERGE.equals(name)) { //處理<merge />標籤 throw new InflateException("<merge /> must be the root element"); } else if (TAG_1995.equals(name)) { //處理<blink />標籤 final View view = new BlinkLayout(mContext, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs, true); viewGroup.addView(view, params); } else { //根據節點名構建一個View例項物件 final View view = createViewFromTag(parent, name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; //呼叫generateLayoutParams()方法返回一個LayoutParams例項物件, final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs, true); //繼續遞迴呼叫 viewGroup.addView(view, params); //OK,將該View以特定LayoutParams值新增至父View } } if (finishInflate) parent.onFinishInflate(); //完成解析過程,通知.. }
在rInflate方法的37行程式碼中,final View view = createViewFromTag(parent, name, attrs),由節點名等引數構建的一個view例項物件,由於下面的程式碼會越來越大,就直接貼出主要實現函式,具體可參見Android原始碼。
/** * default visibility so the BridgeInflater can override it. */ View createViewFromTag(View parent, String name, AttributeSet attrs) { //... try { //... if (view == null) { if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } return view; } catch (InflateException e) { //... } }
然後再往onCreateView()中跟下去,會發現,它其實主要還是實現了createView();所以我們直接CreateView實現。
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs); }
在createView(name, “android.view.”, attrs)中,會用反射機制建立android.view.XXX(比如TextView)的例項物件,並返回。
這也就是LayoutInflater.inflate的佈局解析流程了。
當你熟悉了流程,接下來為你講解的,隨心桌布的動態解析佈局思路,你就基本懂的大半了!
由於最初的layoutInflater.inflate(R.layout.main, null,false)函式,傳入的是R.layout.main資源id,而對於我們的專案,佈局檔案是線上更新的,是間接儲存在sd卡中,所以這種解析方式就不行了,所幸LayoutInflater的api方法還提供了inflate(XmlPullParser parser, ViewGroup root,boolean attachToRoot)解析,根據檔案儲存路徑生成需要的xmlPullParser:
public XmlPullParser getXmlPullParser(String resource) { XmlPullParser parser = Xml.newPullParser(); try { // InputStream is=mContext.getAssets().open("transfer_main.xml"); FileInputStream is = new FileInputStream(resource); parser.setInput(is, "utf-8"); } catch (Exception e) { e.printStackTrace(); } return parser; }
後面的第二步等其實都是一樣的流程(我會在後面貼出實現demo)。但是有一點需要特別注意,也是必須實現的一點:
因為所下載的佈局檔案得到的解析流,跟程式裡res/layout/xxx.xml有一個非常大的不同,資源id!!程式中的佈局檔案,裡面所註冊的id以及text,或者drawble都是可以真實在R目錄下找到的,而下載的佈局檔案是沒有這個福利的,它是我們外面生成的,並沒有經過apk編譯過程。所以為了能得到下載檔案裡的佈局各個id,我們需要自己實現對View的屬性解析。
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { // return createView(name, "android.view.", attrs); return createView(name, "com.xxx.xxxx.viewanalysis.view.VA", attrs); //比如com.xx.view.VATextView 專案中自定義view 繼承自TextView }
佈局中的控制元件程式碼編寫,可以是TextView等原生的,也可以是自定義過的,因為TextView經過加字首,再通過後面的反射方法,也會跑到相應的自定義方法,返回一個自定義View物件。不過因為最終都是要跑自定義view,所以要求我們需要事先定義好會用到的,目前我定義好了9種,比如Button,GridView,RelativeLayout等。如果沒有定義的view直接用在檔案中,會導致編譯出錯(ClassNotFoundException)。
VAButton類實現:
public class VAButton extends android.widget.Button { public VAButton(Context context, AttributeSet attrs) { super(context); setAttributeSet(attrs); } @SuppressWarnings("deprecation") public void setAttributeSet(AttributeSet attrs) { HashMap<String, ParamValue> map = YDResource.getInstance().getViewMap(); int count = attrs.getAttributeCount(); for (int i = 0; i < count; i++) { ParamValue key = map.get(attrs.getAttributeName(i)); if (key == null) { continue; } switch (key) { case id: this.setTag(attrs.getAttributeValue(i)); break; case text: String value = YDResource.getInstance().getString( attrs.getAttributeValue(i)); this.setText(value); break; //case... default: break; } } }
Ps: view id通過設定標誌在外面可以獲取到(具體可以看demo)
屬性方面的解析,我就不多說了,網上的資料很多,有興趣的可以去了解下。
demo效果圖:
專案中的效果圖:
接下來看看demo中的程式碼結構:
這裡的程式碼主體在於YDLayoutInflater與ParamValue的實現,以及9種自定義view。
YDLayoutInflater主要是仿照LayoutInflater實現的,做了一些適應佈局檔案的修改處理,上面已經說過了。
動態解析佈局的思路講完了,應用到專案中,效果也不錯,雖然有一些限制規範,但是對於總體的功能設計是無關大雅的。關於主題呈現動態更新,如果有大神有更好的方式,請私信我哦!
相關文章
- Android 主題動態切換框架:PrismAndroid框架
- echarts 主題動態切換Echarts
- 仿天貓App實現商品列表佈局切換效果APP
- 實現Vue專案主題切換Vue
- Sass應用之實現主題切換
- Spring實現多資料來源動態切換Spring
- Android動態改變佈局Android
- flex佈局---標籤切換Flex
- Android 實現切換主題皮膚功能(類似於眾多app中的 夜間模式,主題包等)AndroidAPP模式
- 專案要實現多資料來源動態切換,咋搞?
- Android 實現APP可切換多語言AndroidAPP
- Flutter UI使用Provide實現主題切換FlutterUIIDE
- HeyUI元件庫 | 如何實現線上切換主題UI元件
- 實現自動切換主題的 VSCode 擴充套件VSCode套件
- Android 頁面多狀態佈局管理Android
- css實現高度動態變化的佈局CSS
- 如何透過css變數實現主題切換?CSS變數
- Dledger是如何實現主從自動切換的
- CSS多種佈局方式自我實現-水平佈局(二)CSS
- (九)主題切換
- android實現多圖片放大縮小的切換Android
- [貝聊科技]談談 iOS 如何動態切換 APP 的主題iOSAPP
- 非常規 - VUE 實現特定場景的主題切換Vue
- nacos實現對minio的動態版本切換
- mybatis 多資料來源動態切換MyBatis
- android: 動態載入碎片佈局的技巧Android
- vue 實現tab切換動態載入不同的元件Vue元件
- [MAUI]模仿iOS多工切換卡片滑動的互動實現UIiOS
- GRIDPANEL動態佈局
- Android 右滑隱藏佈局、上下滑切換顯示資料Android
- 使用 CSS columns 佈局來實現自動分組佈局CSS
- 小程式切換主題配色
- MHA實現mysql主從資料庫手動切換的方法MySql資料庫
- DRBD+Pacemaker實現DRBD主從角色的自動切換薦
- Android 之夜間模式(多主題)的實現思路Android模式
- ostgreSQL主從切換-手動SQL
- Spring 多資料來源 AOP 動態切換Spring
- Android實現RecyclerView巢狀流式佈局AndroidView巢狀