Android中標(biāo)簽容器控件的實(shí)例詳解
前言
在一些APP中我們可以看到一些存放標(biāo)簽的容器控件,和我們平時(shí)使用的一些布局方式有些不同,它們一般都可以自動(dòng)適應(yīng)屏幕的寬度進(jìn)行布局,根據(jù)對(duì)自定義控件的一些理解,今天寫一個(gè)簡(jiǎn)單的標(biāo)簽容器控件,給大家參考學(xué)習(xí)。
下面這個(gè)是我在手機(jī)上截取的一個(gè)實(shí)例,是在MIUI8系統(tǒng)上截取的
這個(gè)是我實(shí)現(xiàn)的效果圖
原理介紹
根據(jù)對(duì)整個(gè)控件的效果分析,大致可以將控件分別從以下這幾個(gè)角度進(jìn)行分析:
1.首先涉及到自定義的ViewGroup,因?yàn)楝F(xiàn)有的控件沒法滿足我們的布局效果,就涉及到要重寫onMeasure和onLayout,這里需要注意的問題是自定義View的時(shí)候,我們需要考慮到View的Padding屬性,而在自定義ViewGroup中我們需要在onLayout中考慮Child控件的margin屬性否則子類設(shè)置這個(gè)屬性將會(huì)失效。整個(gè)View的繪制流程是這樣的:
最頂層的ViewRoot執(zhí)行performTraversals然后分別開始對(duì)各個(gè)View進(jìn)行層級(jí)的測(cè)量、布局、繪制,整個(gè)流程是一層一層進(jìn)行的,也就是說(shuō)父視圖測(cè)量時(shí)會(huì)調(diào)用子視圖的測(cè)量方法,子視圖調(diào)孫視圖方法,一直測(cè)量到葉子節(jié)點(diǎn),performTraversals這個(gè)函數(shù)翻譯過來(lái)很直白,執(zhí)行遍歷,就說(shuō)明了這種層級(jí)關(guān)系。
2.該控件形式上和ListView的形式比較相近,所以在這里我也模仿ListView的Adapter模式實(shí)現(xiàn)了對(duì)控件內(nèi)容的操作,這里對(duì)ListView的setAdapter和Adapter的notifyDataSetChanged方法做個(gè)簡(jiǎn)單的解釋:
在ListView調(diào)用setAdapter后,ListView會(huì)去注冊(cè)一個(gè)Observer對(duì)象到這個(gè)adapter上,然后當(dāng)我們?cè)诟淖冊(cè)O(shè)置到adapter上的數(shù)據(jù)發(fā)改變時(shí),我們會(huì)調(diào)用adapter的notifyDataSetChanged方法,這個(gè)方法就會(huì)通知所有監(jiān)聽了該Adapter數(shù)據(jù)改變時(shí)的Observer對(duì)象,這就是典型的監(jiān)聽者模式,這時(shí)由于ListView中的內(nèi)部成員對(duì)象監(jiān)聽了該事件,就可以知道數(shù)據(jù)源發(fā)生了改變,我們需要對(duì)真?zhèn)€控件重新進(jìn)行繪制了,下面來(lái)一些相關(guān)的源碼。
Adapter的notifyDataSetChanged
public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); }
ListView的setAdapter方法
@Override public void setAdapter(ListAdapter adapter) { /** *每次設(shè)置新的適配的時(shí)候,如果現(xiàn)在有的話會(huì)做一個(gè)解除監(jiān)聽的操作 */ if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } resetList(); mRecycler.clear(); /** 省略部分代碼..... */ if (mAdapter != null) { mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); checkFocus(); /** *在這里對(duì)adapter設(shè)置了監(jiān)聽, *使用的是AdapterDataSetObserver類的對(duì)象,該對(duì)象定義在ListView的父類AdapterView中 */ mDataSetObserver = new AdapterDataSetObserver(); mAdapter.registerDataSetObserver(mDataSetObserver); /** 省略 */ } else { /** 省略 */ } requestLayout(); }
AdapterView中的內(nèi)部類AdapterDataSetObserver
class AdapterDataSetObserver extends DataSetObserver { private Parcelable mInstanceState = null; @Override public void onChanged() { /* ***代碼略*** */ checkFocus(); requestLayout(); } @Override public void onInvalidated() { /* ***代碼略*** */ checkFocus(); requestLayout(); } public void clearSavedState() { mInstanceState = null; } }
一段偽代碼表示
ListView{ Observer observer{ onChange(){ change; } } setAdapter(Adapter adapter){ adapter.register(observer); } } Adapter{ List<Observer> mObservable; register(observer){ mObservable.add(observer); } notifyDataSetChanged(){ for(i-->mObserverable.size()){ mObserverable.get(i).onChange } } }
實(shí)現(xiàn)過程
獲取ViewItem的接口
package humoursz.gridtag.test.adapter; import android.view.View; import java.util.List; /** * Created by zhangzhiquan on 2016/7/19. */ public interface GrideTagBaseAdapter { List<View> getViews(); }
抽象適配器AbsGridTagsAdapter
package humoursz.gridtag.test.adapter; import android.database.DataSetObservable; import android.database.DataSetObserver; /** * Created by zhangzhiquan on 2016/7/19. */ public abstract class AbsGridTagsAdapter implements GrideTagBaseAdapter { DataSetObservable mObservable = new DataSetObservable(); public void notification(){ mObservable.notifyChanged(); } public void registerObserve(DataSetObserver observer){ mObservable.registerObserver(observer); } public void unregisterObserve(DataSetObserver observer){ mObservable.unregisterObserver(observer); } }
此效果中的需要的適配器,實(shí)現(xiàn)了getView接口,主要是模仿了ListView的BaseAdapter
package humoursz.gridtag.test.adapter; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import java.util.ArrayList; import java.util.List; import humoursz.gridtag.test.R; import humoursz.gridtag.test.util.UIUtil; import humoursz.gridtag.test.widget.GridTagView; /** * Created by zhangzhiquan on 2016/7/19. */ public class MyGridTagAdapter extends AbsGridTagsAdapter { private Context mContext; private List<String> mTags; public MyGridTagAdapter(Context context, List<String> tags) { mContext = context; mTags = tags; } @Override public List<View> getViews() { List<View> list = new ArrayList<>(); for (int i = 0; i < mTags.size(); i++) { TextView tv = (TextView) LayoutInflater.from(mContext) .inflate(R.layout.grid_tag_item_text, null); tv.setText(mTags.get(i)); GridTagView.LayoutParams lp = new GridTagView .LayoutParams(GridTagView.LayoutParams.WRAP_CONTENT ,GridTagView.LayoutParams.WRAP_CONTENT); lp.margin(UIUtil.dp2px(mContext, 5)); tv.setLayoutParams(lp); list.add(tv); } return list; } }
最后是主角GridTagsView控件
package humoursz.gridtag.test.widget; import android.content.Context; import android.database.DataSetObserver; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import java.util.List; import humoursz.gridtag.test.adapter.AbsGridTagsAdapter; /** * Created by zhangzhiquan on 2016/7/18. */ public class GridTagView extends ViewGroup { private int mLines = 1; private int mWidthSize = 0; private AbsGridTagsAdapter mAdapter; private GTObserver mObserver = new GTObserver(); public GridTagView(Context context) { this(context, null); } public GridTagView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public GridTagView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void setAdapter(AbsGridTagsAdapter adapter) { if (mAdapter != null) { mAdapter.unregisterObserve(mObserver); } mAdapter = adapter; mAdapter.registerObserve(mObserver); mAdapter.notification(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int curWidthSize = 0; int childHeight = 0; mLines = 1; for (int i = 0; i < getChildCount(); ++i) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); curWidthSize += getChildRealWidthSize(child); if (curWidthSize > widthSize) { /** * 計(jì)算一共需要多少行,用于計(jì)算控件的高度 * 計(jì)算方法是,如果當(dāng)前控件放下后寬度超過 * 容器本身的高度,就放到下一行 */ curWidthSize = getChildRealWidthSize(child); mLines++; } if (childHeight == 0) { /** * 在第一次計(jì)算時(shí)拿到字視圖的高度作為計(jì)算基礎(chǔ) */ childHeight = getChildRealHeightSize(child); } } mWidthSize = widthSize; setMeasuredDimension(widthSize, childHeight == 0 ? heightSize : childHeight * mLines); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() == 0) return; int childCount = getChildCount(); LayoutParams lp = getChildLayoutParams(getChildAt(0)); /** * 初始的左邊界在自身的padding left和child的margin后 * 初始的上邊界原理相同 */ int left = getPaddingLeft() + lp.leftMargin; int top = getPaddingTop() + lp.topMargin; int curLeft = left; for (int i = 0; i < childCount; ++i) { View child = getChildAt(i); int right = curLeft + getChildRealWidthSize(child); /** * 計(jì)算如果放下當(dāng)前試圖后整個(gè)一行到右側(cè)的距離 * 如果超過控件寬那就放到下一行,并且左邊距還原,上邊距等于下一行的開始 */ if (right > mWidthSize) { top += getChildRealHeightSize(child); curLeft = left; } child.layout(curLeft, top, curLeft + child.getMeasuredWidth(), top + child.getMeasuredHeight()); /** * 下一個(gè)控件的左邊開始距離是上一個(gè)控件的右邊 */ curLeft += getChildRealWidthSize(child); } } /** * 獲取childView實(shí)際占用寬度 * @param child * @return 控件實(shí)際占用的寬度,需要算上margin否則margin不生效 */ private int getChildRealWidthSize(View child) { LayoutParams lp = getChildLayoutParams(child); int size = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; return size; } /** * 獲取childView實(shí)際占用高度 * @param child * @return 實(shí)際占用高度需要考慮上下margin */ private int getChildRealHeightSize(View child) { LayoutParams lp = getChildLayoutParams(child); int size = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; return size; } /** * 獲取LayoutParams屬性 * @param child * @return */ private LayoutParams getChildLayoutParams(View child) { LayoutParams lp; if (child.getLayoutParams() instanceof LayoutParams) { lp = (LayoutParams) child.getLayoutParams(); } else { lp = (LayoutParams) generateLayoutParams(child.getLayoutParams()); } return lp; } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attr) { return new LayoutParams(getContext(), attr); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } public static class LayoutParams extends MarginLayoutParams { public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(MarginLayoutParams source) { super(source); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } public void marginLeft(int left) { this.leftMargin = left; } public void marginRight(int r) { this.rightMargin = r; } public void marginTop(int t) { this.topMargin = t; } public void marginBottom(int b) { this.bottomMargin = b; } public void margin(int m){ this.leftMargin = m; this.rightMargin = m; this.topMargin = m; this.bottomMargin = m; } } private class GTObserver extends DataSetObserver { @Override public void onChanged() { removeAllViews(); List<View> list = mAdapter.getViews(); for (int i = 0; i < list.size(); i++) { addView(list.get(i)); } } @Override public void onInvalidated() { Log.d("Mrz","fd"); } } }
MainActivity
package humoursz.gridtag.test; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import java.util.List; import humoursz.gridtag.test.adapter.MyGridTagAdapter; import humoursz.gridtag.test.util.ListUtil; import humoursz.gridtag.test.widget.GridTagView; public class MainActivity extends AppCompatActivity { MyGridTagAdapter adapter; GridTagView mGridTag; List<String> mList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mGridTag = (GridTagView)findViewById(R.id.grid_tags); mList = ListUtil.getGridTagsList(20); adapter = new MyGridTagAdapter(this,mList); mGridTag.setAdapter(adapter); } public void onClick(View v){ mList.removeAll(mList); mList.addAll(ListUtil.getGridTagsList(20)); adapter.notification(); } }
XML 文件
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="humoursz.gridtag.test.MainActivity"> <humoursz.gridtag.test.widget.GridTagView android:id="@+id/grid_tags" android:layout_width="match_parent" android:layout_height="wrap_content"> </humoursz.gridtag.test.widget.GridTagView> <Button android:layout_centerInParent="true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="onClick" android:text="換一批"/> </RelativeLayout>
以上就是Android中標(biāo)簽容器控件的全部實(shí)現(xiàn)過程,這樣一個(gè)簡(jiǎn)單的控件就寫好了,主要需要注意measure和layout否則很多效果都會(huì)失效,安卓中的LinearLayout之類的控件實(shí)際實(shí)現(xiàn)起來(lái)要復(fù)雜的很多,因?yàn)橹С值膶傩詫?shí)在的太多了,多動(dòng)手實(shí)踐可以幫助理解,希望本文能幫助到在Android開發(fā)中的大家。
- 從源碼解析Android中View的容器ViewGroup
- Android應(yīng)用開發(fā)中自定義ViewGroup視圖容器的教程
- Android自定義ViewGroup實(shí)現(xiàn)標(biāo)簽流容器FlowLayout
- Android自定義控件之繼承ViewGroup創(chuàng)建新容器
- Android中實(shí)現(xiàn)多行、水平滾動(dòng)的分頁(yè)的Gridview實(shí)例源碼
- android listview 水平滾動(dòng)和垂直滾動(dòng)的小例子
- Android使用RecyclerView實(shí)現(xiàn)水平滾動(dòng)控件
- Android實(shí)現(xiàn)Activity水平和垂直滾動(dòng)條的方法
- 詳解Android使GridView橫向水平滾動(dòng)的實(shí)現(xiàn)方式
- Android使用Recyclerview實(shí)現(xiàn)圖片水平自動(dòng)循環(huán)滾動(dòng)效果
- Android開發(fā)實(shí)現(xiàn)自定義水平滾動(dòng)的容器示例
相關(guān)文章
android studio library 模塊中正確引用aar的實(shí)例講解
下面小編就為大家分享一篇android studio library 模塊中正確引用aar的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2018-01-01Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫效果(六)
這篇文章主要為大家詳細(xì)介紹了Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫效果第六篇,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08Win8下Android SDK安裝與環(huán)境變量配置教程
這篇文章主要為大家詳細(xì)介紹了Win8下Android SDK安裝與環(huán)境變量配置教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07AndroidX下使用Activity和Fragment的變化詳解
這篇文章主要介紹了AndroidX下使用Activity和Fragment的變化詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04Android開發(fā)之使用150行代碼實(shí)現(xiàn)滑動(dòng)返回效果
本文給大家分享Android開發(fā)之使用150行代碼實(shí)現(xiàn)滑動(dòng)返回效果的代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2019-05-05Android自定義StickinessView粘性滑動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android自定義StickinessView粘性滑動(dòng)效果的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03