Android開發之漫漫長途 番外篇——自訂View的各種姿勢2

來源:互聯網
上載者:User

標籤:clear   自己   函數   app   over   準備工作   and   重寫   view   

該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,我會盡量按照先易後難的順序進行編寫該系列。該系列引用了《Android開發藝術探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相關知識,另外也借鑒了其他的優質部落格,在此向各位大神表示感謝,膜拜!!!另外,本系列文章知識可能需要有一定Android開發基礎和項目經驗的同學才能更好理解,也就是說該系列文章面向的是Android中進階開發工程師。

上一篇我們詳細分析了Android事件體系。也從源碼角度徹底瞭解了為何會有如此表現。不僅要知其然,更要知其所以然。那麼本篇呢,我們依然是來自訂View。與上一番外篇不同的是本章的重點放在ViewGroup上。我們知道ViewGroup是View的子類,Android系統中有許多控制項繼承自ViewGroup的控制項。比如我們常用的FrameLayout、LinearLayout、RelativeLayout等布局都是繼承自ViewGroup。自訂ViewGroup難度比較大,是因為ViewGroup要管理子View的測量、布局等。關於View的測量、布局、繪製我們在Android開發之漫漫長途 Ⅴ——Activity的顯示之ViewRootImpl的PreMeasure、WindowLayout、EndMeasure、Layout、Draw已經詳細分析了。

註:我真是給自己挖了個大坑,關於自訂ViewGroup的執行個體我想了好久也找了好久。發現想要實現一個很有規範的自訂View是有一定代價的,這點你看看LinearLayout等系統本身的ViewGroup控制項的源碼就知道,他們的實現都很複雜。想選擇一個較簡單的把,又不想注水,較難的把,感覺又繞進了代碼的死胡同。不能讓讀者對自訂ViewGroup的核心有個更清晰的認識。一直拖到今天,真是要對大家說抱歉。好了,這些“矯情”的話就不多說了,我們還是來看下面的執行個體把。

實現流式布局FlowLayout

我在拉勾網App上搜尋公司或者職位的下方發現一個效果

拉勾網這些顯示的具體資料怎麼來的我們不討論,我們試著來實現一下它的這個布局效果。

處於上方的Tag“猜你喜歡”、“熱門公司”可以用一個TextView顯示,我們忽略它。關鍵是下方的標籤流式布局。我們就來分析它。

  • 首先流式布局中的標籤應該是個TextView,關於它下方的橢圓形邊界,我們可以為其制定background

layout/tag_view.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:layout_margin="5dp"          android:background="@drawable/tag_bg"          android:text="Helloworld"          android:textSize="15sp"          android:textColor="@drawable/text_color"></TextView>

drawable/tag_bg.xml

<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android">    <item        android:drawable="@drawable/checked_bg"        android:state_checked="true"        >    </item>    <item          android:drawable="@drawable/normal_bg"></item></selector>

drawable/checked_bg.xml

<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android">    <solid android:color="#88888888"/>    <corners android:radius="30dp"/>    <padding        android:bottom="2dp"        android:left="10dp"        android:right="10dp"        android:top="2dp"/></shape>

drawable/normal_bg.xml

<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android" >    <solid android:color="#ffffff" />    <corners android:radius="30dp" />    <stroke android:color="#88888888"  android:width="1dp"/>    <padding        android:bottom="2dp"        android:left="10dp"        android:right="10dp"        android:top="2dp" /></shape>

drawable/text_color.xml

<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android">    <item android:color="#888888"/></selector>

上方布局可得到如下預覽

至此我們的準備工作已經完畢。

  • 自訂ViewGroup(重點)

上面我們已經得到了一個布局檔案達到了我們流式布局中的子View的顯示效果。那我們下面就來自訂ViewGroup來實現上述的流式布局。
① 首先繼承自ViewGroup,繼承自ViewGroup重寫其建構函式以及onLayout方法,我們使用AndroidStudio提示就行了

public class MyTagFlowLayout extends ViewGroup {  public MyTagFlowLayout(Context context) {       this(context, null);   }   public MyTagFlowLayout(Context context, AttributeSet attrs) {       this(context, attrs,0);   }   public MyTagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {       this(context, attrs,defStyleAttr,0);   }      @SuppressLint("NewApi")   public MyTagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {       super(context, attrs, defStyleAttr, defStyleRes); @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        }}

② 初始化一些資訊

//由可知,我們可將上面的流式布局分為三部分//每一行的View 組成的List private List<View> lineViews = new ArrayList<>(); //每一行的高度 組成的List private List<Integer> mLineHeight = new ArrayList<Integer>(); //所有的View  private List<List<View>> mAllViews = new ArrayList<List<View>>(); //適配器 private MyTagAdapter mTagAdapter;

我們先搞定適配器,我們提供一個數組資訊

//需要顯示的資料 private String[] mGuseeYourLoveVals = new String[]            {"Android", "Android移動", "Java", "UI設計師", "android實習",                    "android 移動","android安卓","安卓"};

適配器的實現十分簡單,我們可以仿照Android系統自有的適配器

/**    抽象類別*/public abstract class MyTagAdapter<T> {    //資料    private List<T> mTagDatas;    //建構函式    public MyTagAdapter(T[] datas) {        mTagDatas = new ArrayList<T>(Arrays.asList(datas));    }    //擷取總數    public int getCount() {        return mTagDatas == null ? 0 : mTagDatas.size();    }    //抽象方法 擷取View 由子類具體實現如何獲得View    public abstract View getView(MyTagFlowLayout parent, int position, T t);    //擷取資料中的某個Item    public T getItem(int position) {        return mTagDatas.get(position);    }}

我們在MainActivity中調用如下語句

//MyTagFlowLayout使我們自訂的ViewGroup,目前該類還是預設實現mGuseeYourLoveFlowLayout = (MyTagFlowLayout) findViewById(R.id.id_guess_your_love);//指定適配器,我們這裡使用了匿名內部類的方式指定mGuseeYourLoveFlowLayout.setAdapter(new MyTagAdapter<String>(mGuseeYourLoveVals){    //擷取LayoutInflater     final LayoutInflater mInflater = LayoutInflater.from(MainActivity.this);    //重點來了,我們在該匿名內部類中實現了MyTagAdapter的getView方法    @Override    public View getView(MyTagFlowLayout parent, int position, String s)    {        //在該方法中我們去載入了我們上面提到的layout/tag_view.xml,並返回TextView         TextView tv = (TextView) mInflater.inflate(R.layout.tag_view,                mGuseeYourLoveFlowLayout, false);        tv.setText(s);        return tv;    }});

其中MyTagFlowLayout的setAdapter方法如下,,我們一點點分析MyTagFlowLayout定義過程

public void setAdapter(MyTagAdapter adapter) {    removeAllViews();//先清空MyTagFlowLayout下的所有View    for (int i = 0; i < adapter.getCount(); i++) {        //這裡的tagView 就是剛才的TextView        View tagView = adapter.getView(this, i, adapter.getItem(i));        //添加View        addView(tagView);    }}

此時我們的MyTagFlowLayout資料已經載入完畢,接下來就是顯示,,顯示才是重中之重
我們先來複習一下View的顯示過程measure->layout->draw。那麼顯然我們這個要先measure,那就重寫onMeasure方法把

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        //這裡我們先擷取父View給定的測量參數,注意這個父View代表的是MyTagFlowLayout的父View        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);//擷取父View傳給MyTagFlowLayout的寬度        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);//擷取父View傳給MyTagFlowLayout的寬度測量模式        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);//擷取父View傳給MyTagFlowLayout的高度        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);//擷取父View傳給MyTagFlowLayout的高度測量模式               int width = 0;        int height = 0;        int lineWidth = 0;        int lineHeight = 0;        //得到所有的子View,在上一步的過程中我們已經添加的子View,按照上一步的資料,這裡的cCount 應該是8        int cCount = getChildCount();                for (int i = 0; i < cCount; i++) {            //迴圈得到每一個子View,這個的child指向的實際是我們上面添加TextView            View child = getChildAt(i);             //測量每一個子View,            measureChild(child, widthMeasureSpec, heightMeasureSpec);            //得到每一個子View的測量寬度和高度            int childWidth = child.getMeasuredWidth();            int childHeight = child.getMeasuredHeight();            //如果當前行的寬度+將要添加的child的寬度 > MyTagFlowLayout的寬度-pading,說明當前行已經“滿”了,這個“滿”了意思是,當前行已經容納不了下一個子View            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {//"滿"了需要換行                width = Math.max(width, lineWidth);//MyTagFlowLayout的寬度取上一次寬度和當前lineWidth的最大值                lineWidth = childWidth;//重設當前行的lineWidth                 height += lineHeight;//MyTagFlowLayout的高度增加                lineHeight = childHeight;//重設當前行的lineHeight 為子View的高度            } else {//沒“滿”,當前行可以容納下一個子View                lineWidth += childWidth;//當前行的寬度增加                lineHeight = Math.max(lineHeight, childHeight);//當前行的高度取上一次高度和子View的高度的最大值            }            if (i == cCount - 1) {//如果當前View是最後的View                width = Math.max(lineWidth, width);//MyTagFlowLayout的寬度取上一次寬度和當前lineWidth的最大值                height += lineHeight;//MyTagFlowLayout的高度增加            }        }        //設定MyTagFlowLayout的高度和寬度        //如果是在XMl指定了MyTagFlowLayout的寬度,如 android:layout_width="40dp"那就使用指定的寬度,否則使用測量的寬度-padding,高度的設定與寬度雷同        setMeasuredDimension(                modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()        );    }

上面我們已經分析了onMeasure方法,measure是測量,後面的layout是布局,我們來看一下布局

@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {    //先清除所有的List    mAllViews.clear();    mLineHeight.clear();    lineViews.clear();    //得到MyTagFlowLayout的寬度,這個我們已經在onMeasure方法中得到了    int width = getWidth();    //行寬和行高初始化為0    int lineWidth = 0;    int lineHeight = 0;    //一樣的得到所有子View的數量    int cCount = getChildCount();    for (int i = 0; i < cCount; i++) {        //迴圈得到每一個子View,這個的child指向的實際是我們上面添加TextView        View child = getChildAt(i);        //View 可見度如果是View.GONE,則忽略它        if (child.getVisibility() == View.GONE) continue;        //得到子View的測量寬度和高度        int childWidth = child.getMeasuredWidth();        int childHeight = child.getMeasuredHeight();        //如果當前行寬lineWidth + 當前子View的寬度 > MyTagFlowLayout的寬度-padding,那麼我們該換行顯示了        if (childWidth + lineWidth > width - getPaddingLeft() - getPaddingRight()) {            mLineHeight.add(lineHeight);//把當前行高lineHeight添加進表示當前所有行 行高表示的mLineHeight list中            mAllViews.add(lineViews);//同樣的加入mAllViews            lineWidth = 0;//重設行寬            lineHeight = childHeight;//重設行高            lineViews = new ArrayList<View>();//重設lineViews         }               lineWidth += childWidth;//當前行寬lineWidth 增加        lineHeight = Math.max(lineHeight, childHeight );;//當前行高lineHeight 取前一次行高和子View的最大值        lineViews.add(child);//把子View添加進表示當前所有子View的lineViews的list中    }    mLineHeight.add(lineHeight);//把當前行高lineHeight添加進表示當前所有行 行    mAllViews.add(lineViews);//同樣的加入mAllViews    //擷取PaddingTop    int top = getPaddingTop();    //擷取所有行的數量    int lineNum = mAllViews.size();        for (int i = 0; i < lineNum; i++) {        //迴圈取出每一行        lineViews = mAllViews.get(i);        //迴圈去除每一行的行高        lineHeight = mLineHeight.get(i);        //擷取PaddingLeft        int left = getPaddingLeft();                for (int j = 0; j < lineViews.size(); j++) {            //從每一行中迴圈取出子View            View child = lineViews.get(j);            if (child.getVisibility() == View.GONE) {                continue;            }            //調用child的layout,這裡實際上是調用TextView.layout            child.layout(left, top, lc + child.getMeasuredWidth(), tc + child.getMeasuredHeight());                        left += child.getMeasuredWidth() ;//left遞增        }        top += lineHeight;//top遞增    }}

好了,我們來運行一下

效果並不像我們在文章開頭給出的那樣,,但是起碼出來一個類似的了。下面要考慮的就是如何為這些子View添加合適的間距了。。我相信聰明的讀者一定可以自行解決這個問題的。這裡稍微提示一下間距->margin?? 如有疑問,請留言。

本篇總結
本篇文章我們初探了自訂ViewGroup的一些知識和思想,很遺憾,該篇文章中許多代碼並不是最佳實務,希望各位讀者雅正。而且關於View的事件問題,我找了好久實在找不出好的例子來這裡分享給大家,如果大家有好的想法,請在評論區砸我吧,最好是把View的繪製體系和事件體系完美結合簡單明了、“活血化瘀”自訂ViewGroup的執行個體。。我在這裡向被辜負期望的讀者們道歉。

下篇預告
如果有人提供想法,那麼下一篇我們還是來自訂ViewGroup,如果沒有,(我的部落格貌似一直很少人評論),我們就來稍微歇歇,,看看平常開發中經常遇到的記憶體流失及相關解決辦法。

此致,敬禮

Android開發之漫漫長途 番外篇——自訂View的各種姿勢2

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.