Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android標簽容器控件的實現

Android標簽容器控件的實現

編輯:關於Android編程

介紹

在一些APP中我們可以看到一些存放標簽的容器控件,和我們平時使用的一些布局方式有些不同,它們一般都可以自動適應屏幕的寬度進行布局,根據對自定義控件的一些理解,今天寫一個簡單的標簽容器控件,項目源碼在最後給出

下面這個是我在手機上截取的一個實例,是在MIUI8系統上截取的

這裡寫圖片描述

這個是我實現的效果圖

這裡寫圖片描述

原理介紹

根據對整個控件的效果分析,大致可以將控件分別從以下這幾個角度進行分析:

1.首先涉及到自定義的ViewGroup,因為現有的控件沒法滿足我們的布局效果,就涉及到要重寫onMeasure和onLayout,這裡需要注意的問題是自定義View的時候,我們需要考慮到View的Padding屬性,而在自定義ViewGroup中我們需要在onLayout中考慮Child控件的margin屬性否則子類設置這個屬性將會失效。整個View的繪制流程是這樣的:

最頂層的ViewRoot執行performTraversals然後分別開始對各個View進行層級的測量、布局、繪制,整個流程是一層一層進行的,也就是說父視圖測量時會調用子視圖的測量方法,子視圖調孫視圖方法,一直測量到葉子節點,performTraversals這個函數翻譯過來很直白,執行遍歷,就說明了這種層級關系。

2.該控件形式上和ListView的形式比較相近,所以在這裡我也模仿ListView的Adapter模式實現了對控件內容的操作,這裡對ListView的setAdapter和Adapter的notifyDataSetChanged方法做個簡單的解釋:

在ListView調用setAdapter後,ListView會去注冊一個Observer對象到這個adapter上,然後當我們在改變設置到adapter上的數據發改變時,我們會調用adapter的notifyDataSetChanged方法,這個方法就會通知所有監聽了該Adapter數據改變時的Observer對象,這就是典型的監聽者模式,這時由於ListView中的內部成員對象監聽了該事件,就可以知道數據源發生了改變,我們需要對真個控件重新進行繪制了,下面來一些相關的源碼。

Adapter的notifyDataSetChanged

public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

ListView的setAdapter方法

@Override
    public void setAdapter(ListAdapter adapter) {
        /**
         *每次設置新的適配的時候,如果現在有的話會做一個解除監聽的操作
         */
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        mRecycler.clear();
        /** 省略部分代碼.....   */
        if (mAdapter != null) {
            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
            checkFocus();

            /**
            *在這裡對adapter設置了監聽,
            *使用的是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 mObservable;
    register(observer){
        mObservable.add(observer);
    }
    notifyDataSetChanged(){
        for(i-->mObserverable.size()){
            mObserverable.get(i).onChange
        }
    }
}

實現過程

獲取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 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);
    }
}

此效果中的需要的適配器,實現了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 mTags;

    public MyGridTagAdapter(Context context, List tags) {
        mContext = context;
        mTags = tags;
    }

    @Override
    public List getViews() {
        List 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 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 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 文件




    
    

這樣一個簡單的控件就寫好了,主要需要注意measure和layout否則很多效果都會失效,安卓中的LinearLayout之類的控件實際實現起來要復雜的很多,因為支持的屬性實在的太多了,多動手實踐可以幫助理解,下面是工程的下載地址

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved