Android中標簽容器控件的實例詳解
前言
在一些APP中我們可以看到一些存放標簽的容器控件,和我們平時使用的一些布局方式有些不同,它們一般都可以自動適應屏幕的寬度進行布局,根據(jù)對自定義控件的一些理解,今天寫一個簡單的標簽容器控件,給大家參考學習。
下面這個是我在手機上截取的一個實例,是在MIUI8系統(tǒng)上截取的

這個是我實現(xiàn)的效果圖

原理介紹
根據(jù)對整個控件的效果分析,大致可以將控件分別從以下這幾個角度進行分析:
1.首先涉及到自定義的ViewGroup,因為現(xiàn)有的控件沒法滿足我們的布局效果,就涉及到要重寫onMeasure和onLayout,這里需要注意的問題是自定義View的時候,我們需要考慮到View的Padding屬性,而在自定義ViewGroup中我們需要在onLayout中考慮Child控件的margin屬性否則子類設置這個屬性將會失效。整個View的繪制流程是這樣的:
最頂層的ViewRoot執(zhí)行performTraversals然后分別開始對各個View進行層級的測量、布局、繪制,整個流程是一層一層進行的,也就是說父視圖測量時會調用子視圖的測量方法,子視圖調孫視圖方法,一直測量到葉子節(jié)點,performTraversals這個函數(shù)翻譯過來很直白,執(zhí)行遍歷,就說明了這種層級關系。
2.該控件形式上和ListView的形式比較相近,所以在這里我也模仿ListView的Adapter模式實現(xiàn)了對控件內容的操作,這里對ListView的setAdapter和Adapter的notifyDataSetChanged方法做個簡單的解釋:
在ListView調用setAdapter后,ListView會去注冊一個Observer對象到這個adapter上,然后當我們在改變設置到adapter上的數(shù)據(jù)發(fā)改變時,我們會調用adapter的notifyDataSetChanged方法,這個方法就會通知所有監(jiān)聽了該Adapter數(shù)據(jù)改變時的Observer對象,這就是典型的監(jiān)聽者模式,這時由于ListView中的內部成員對象監(jiān)聽了該事件,就可以知道數(shù)據(jù)源發(fā)生了改變,我們需要對真?zhèn)€控件重新進行繪制了,下面來一些相關的源碼。
Adapter的notifyDataSetChanged
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
ListView的setAdapter方法
@Override
public void setAdapter(ListAdapter adapter) {
/**
*每次設置新的適配的時候,如果現(xiàn)在有的話會做一個解除監(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();
/**
*在這里對adapter設置了監(jiān)聽,
*使用的是AdapterDataSetObserver類的對象,該對象定義在ListView的父類AdapterView中
*/
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
/** 省略 */
} else {
/** 省略 */
}
requestLayout();
}
AdapterView中的內部類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
}
}
}
實現(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);
}
}
此效果中的需要的適配器,實現(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) {
/**
* 計算一共需要多少行,用于計算控件的高度
* 計算方法是,如果當前控件放下后寬度超過
* 容器本身的高度,就放到下一行
*/
curWidthSize = getChildRealWidthSize(child);
mLines++;
}
if (childHeight == 0) {
/**
* 在第一次計算時拿到字視圖的高度作為計算基礎
*/
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);
/**
* 計算如果放下當前試圖后整個一行到右側的距離
* 如果超過控件寬那就放到下一行,并且左邊距還原,上邊距等于下一行的開始
*/
if (right > mWidthSize) {
top += getChildRealHeightSize(child);
curLeft = left;
}
child.layout(curLeft, top, curLeft + child.getMeasuredWidth(), top + child.getMeasuredHeight());
/**
* 下一個控件的左邊開始距離是上一個控件的右邊
*/
curLeft += getChildRealWidthSize(child);
}
}
/**
* 獲取childView實際占用寬度
* @param child
* @return 控件實際占用的寬度,需要算上margin否則margin不生效
*/
private int getChildRealWidthSize(View child) {
LayoutParams lp = getChildLayoutParams(child);
int size = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
return size;
}
/**
* 獲取childView實際占用高度
* @param child
* @return 實際占用高度需要考慮上下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中標簽容器控件的全部實現(xiàn)過程,這樣一個簡單的控件就寫好了,主要需要注意measure和layout否則很多效果都會失效,安卓中的LinearLayout之類的控件實際實現(xiàn)起來要復雜的很多,因為支持的屬性實在的太多了,多動手實踐可以幫助理解,希望本文能幫助到在Android開發(fā)中的大家。
- 從源碼解析Android中View的容器ViewGroup
- Android應用開發(fā)中自定義ViewGroup視圖容器的教程
- Android自定義ViewGroup實現(xiàn)標簽流容器FlowLayout
- Android自定義控件之繼承ViewGroup創(chuàng)建新容器
- Android中實現(xiàn)多行、水平滾動的分頁的Gridview實例源碼
- android listview 水平滾動和垂直滾動的小例子
- Android使用RecyclerView實現(xiàn)水平滾動控件
- Android實現(xiàn)Activity水平和垂直滾動條的方法
- 詳解Android使GridView橫向水平滾動的實現(xiàn)方式
- Android使用Recyclerview實現(xiàn)圖片水平自動循環(huán)滾動效果
- Android開發(fā)實現(xiàn)自定義水平滾動的容器示例
相關文章
Android 判斷當前語言環(huán)境是否是中文環(huán)境
本文主要介紹了Android 判斷當前語言環(huán)境是否是中文環(huán)境的方法。具有很好的參考價值。下面跟著小編一起來看下吧2017-04-04
Android 實現(xiàn)永久性開啟adb 的root權限
這篇文章主要介紹了Android 實現(xiàn)永久性開啟adb 的root權限,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android基于socket實現(xiàn)的簡單C/S聊天通信功能
這篇文章主要介紹了Android基于socket實現(xiàn)的簡單C/S聊天通信功能,結合實例形式分析了Android使用socket實現(xiàn)客服端與服務器端數(shù)據(jù)的發(fā)送與接收處理技巧,需要的朋友可以參考下2016-10-10
Android Studio下載、安裝和配置+SDK+tools下載(無敵超級詳細版本)
這篇文章主要介紹了Android Studio下載、安裝和配置+SDK+tools下載(無敵超級詳細版本),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-11-11
Android 獲取drawable目錄圖片 并存入指定文件的步驟詳解
這篇文章主要介紹了Android 獲取drawable目錄圖片 并存入指定文件,本文分步驟通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03
詳解Flutter 調用 Android Native 的方法
這篇文章主要介紹了詳解Flutter 調用 Android Native 的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01
Flutter pageview切換指示器的實現(xiàn)代碼
這篇文章主要介紹了Flutter pageview切換指示器的實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-04-04

