Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義View系列教程05--示例分析

自定義View系列教程05--示例分析

編輯:關於Android編程

 

之前結合源碼分析完了自定義View的三個階段:measure,layout,draw。
那麼,自定義有哪幾種常見的方式呢?

直接繼承自View
在使用該方式實現自定義View時通常的核心操作都在onDraw( )當中進行。但是,請注意,在分析measure部分源碼的時候,我們提到如果直接繼承自View在onMeasure( )中要處理view大小為wrap_content的情況,否則這種情況下的大小和match_parent一樣。除此以為,還需要注意對於padding的處理。

繼承自系統已有的View
比如常見的TextView,Button等等。如果采用該方式,我們只需要在系統控件的基礎上做出一些調整和擴展即可,而且也不需要去自己支持wrap_content和padding。

直接繼承自ViewGroup
如果使用該方式實現自定義View,請注意兩個問題
第一點:
在onMeasure( )實現wrap_content的支持。這點和直接繼承自View是一樣的。
第二點:
在onMeasure( )和onLayout中需要處理自身的padding以及子View的margin

繼承自系統已有的ViewGroup
比如LinearLayout,RelativeLayout等等。如果采用該方式,那麼在3中提到的兩個問題就不用再過多考慮了,簡便了許多。

在此,舉兩個例子。


瞅瞅第一個例子,效果如下圖:

這裡寫圖片描述

這裡寫圖片描述
對於該效果的主要描述如下:

點擊Title部分,展開圖片 再次點擊Title,收縮圖片 圖片的收縮和展開都漸次進行的,並使用動畫切換右側箭頭的方向。

好了,效果已經看到了,我們來明確和拆解一下這個小功能

控件由數字,標題,箭頭,圖片四部分組成 點擊標題逐漸地顯示或隱藏圖片 在圖片的切換過程中伴隨著箭頭方向的改變

弄清楚這些就該動手寫代碼了。
先來看這個控件的布局文件





    

        

        


        
    

    

    
    

請注意,在此將顯示圖片的容器即contentRelativeLayout設置為gone。
為什麼要這麼做呢?因為進入應用後是看不到圖片部分的,只有點擊後才可見。嗯哼,你大概已經猜到了:圖片的隱藏和顯示是通過改變容器的visibility實現的。是的!那圖片的逐漸顯示和隱藏還有箭頭的旋轉又是怎麼做的呢?請看該控件的具體實現。

package com.stay4it.testcollapseview;

import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
/**
 * 原創作者:
 * 谷哥的小弟
 *
 * 博客地址:
 * http://blog.csdn.net/lfdfhl
 */
public class CollapseView extends LinearLayout {
    private long duration = 350;
    private Context mContext;
    private TextView mNumberTextView;
    private TextView mTitleTextView;
    private RelativeLayout mContentRelativeLayout;
    private RelativeLayout mTitleRelativeLayout;
    private ImageView mArrowImageView;
    int parentWidthMeasureSpec;
    int parentHeightMeasureSpec;
    public CollapseView(Context context) {
        this(context, null);
    }

    public CollapseView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext=context;
        LayoutInflater.from(mContext).inflate(R.layout.collapse_layout, this);
        initView();
    }


    private void initView() {
        mNumberTextView=(TextView)findViewById(R.id.numberTextView);
        mTitleTextView =(TextView)findViewById(R.id.titleTextView);
        mTitleRelativeLayout= (RelativeLayout) findViewById(R.id.titleRelativeLayout);
        mContentRelativeLayout=(RelativeLayout)findViewById(R.id.contentRelativeLayout);
        mArrowImageView =(ImageView)findViewById(R.id.arrowImageView);
        mTitleRelativeLayout.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                rotateArrow();
            }
        });

        collapse(mContentRelativeLayout);
    }


    public void setNumber(String number){
        if(!TextUtils.isEmpty(number)){
            mNumberTextView.setText(number);
        }
    }

    public void setTitle(String title){
        if(!TextUtils.isEmpty(title)){
            mTitleTextView.setText(title);
        }
    }

    public void setContent(int resID){
        View view=LayoutInflater.from(mContext).inflate(resID,null);
        RelativeLayout.LayoutParams layoutParams=
                new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        view.setLayoutParams(layoutParams);
        mContentRelativeLayout.addView(view);
    }


    public void rotateArrow() {
        int degree = 0;
        if (mArrowImageView.getTag() == null || mArrowImageView.getTag().equals(true)) {
            mArrowImageView.setTag(false);
            degree = -180;
            expand(mContentRelativeLayout);
        } else {
            degree = 0;
            mArrowImageView.setTag(true);
            collapse(mContentRelativeLayout);
        }
        mArrowImageView.animate().setDuration(duration).rotation(degree);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        parentWidthMeasureSpec=widthMeasureSpec;
        parentHeightMeasureSpec=heightMeasureSpec;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
    }

    // 展開
    private void expand(final View view) {
        WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        view.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);
        final int measuredWidth = view.getMeasuredWidth();
        final int measuredHeight = view.getMeasuredHeight();
        view.setVisibility(View.VISIBLE);

        Animation animation = new Animation() {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                if(interpolatedTime == 1){
                    view.getLayoutParams().height =measuredHeight;
                }else{
                    view.getLayoutParams().height =(int) (measuredHeight * interpolatedTime);
                }
                view.requestLayout();
            }


            @Override
            public boolean willChangeBounds() {
                return true;
            }
        };
        animation.setDuration(duration);
        view.startAnimation(animation);
    }

    // 折疊
    private void collapse(final View view) {
        final int measuredHeight = view.getMeasuredHeight();
        Animation animation = new Animation() {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                if (interpolatedTime == 1) {
                    view.setVisibility(View.GONE);
                } else {
                    view.getLayoutParams().height = measuredHeight - (int) (measuredHeight * interpolatedTime);
                    view.requestLayout();
                }
            }

            @Override
            public boolean willChangeBounds() {
                return true;
            }
        };
        animation.setDuration(duration);
        view.startAnimation(animation);
    }
}

現就該代碼中的主要操作做一些分析和介紹。

控件提供setNumber()方法用於設置最左側的數字,請參見代碼第62-66行。
比如你有三個女朋友,那麼她們的編號分別就是1,2,3 控件提供setTitle()方法用於設置標題,請參見代碼第68-72行
比如,現女友,前女友,前前女友。 控件提供setContent()方法用於設置隱藏和顯示的內容,請參見代碼第74-80行。
請注意,該方法的參數是一個布局文件的ID。所以,要顯示和隱藏的東西只要寫在一個布局文件中就行。這樣就靈活多了,可以根據實際需求實現不同的布局就行。比如在這個例子中,我在一個布局文件中就只放了一個ImageView,然後將這個布局文件的ID傳遞給該方法就行。 實現Title部分Click監聽,請參見代碼第51-56行
在監聽到Click事件後顯示或隱藏content部分

實現content部分的顯示,請參見代碼第110-138行
在這遇到一個難題:
這個content會占多大的空間呢?
我猛地這麼一問,大家可能有點懵圈。
如果沒有聽懂或者回答不上來,我就先舉個例子:
小狗一秒鐘跑1米(即小狗的速度為1m/s),請問小狗跑完這段路要多少時間?
看到這個問題,是不是覺得挺腦殘的,是不是有一種想抽我耳光的沖動?
你他妹的,路程的長短都沒有告訴我,我怎麼知道小狗要跑多久?!真是日了狗了!

嗯哼,是的。我們在這裡根本不知道這個View(比如此處的content)有多高多寬,我們當然也不知道它要占多大的空間!!那怎麼辦呢?在這就按照最直接粗暴的方式來——遇到問題,解決問題!找出該View的寬和高!
前面在分析View的measure階段時我們知道這些控件的寬和高是由系統測量的,在此之後我們只需要利用getMeasuredWidth()和getMeasuredHeight()就行了。但是這個控件的visibility原本是GONE的,系統在measure階段根本不會去測量它的寬和高,所以現在需要我們自己去手動測量。代碼如下:

view.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);

獲取到view的寬高後借助於動畫實現content的漸次展開,請參見代碼第119-137行。
動畫的interpolatedTime在一定時間內(duration)從0變化到1,所以

measuredHeight * interpolatedTime

表示了content的高從0到measuredHeight的逐次變化,在這個變化的過程中不斷調用

view.requestLayout();

刷新界面,這樣就達到了預想的效果。

實現content部分的隱藏,請參見代碼第141-161行
隱藏的過程和之前的逐次顯示過程原理是一樣的,不再贅述。

實現箭頭的轉向,請參見代碼第83-95行
這個比較簡單,在此直接用屬性動畫(ViewPropertyAnimator)讓箭頭旋轉

示例小結:
在該demo中主要采用了手動測量View的方式獲取View的大小。


瞅瞅第二個例子,效果如下圖:

這裡寫圖片描述
嗯哼,這個流式布局(FlowLayout)大家可能見過,它常用來做一些標簽的顯示。比如,我要給我女朋友的照片加上描述,我就可以設置tag為:”賢良淑德”, “女神”, “年輕美貌”, “清純”, “溫柔賢惠”等等。而且在標簽的顯示過程中,如果這一行沒有足夠的空間顯示下一個標簽,那麼會先自動換行然後再添加新的標簽。
好了,效果已經看到了,我們來瞅瞅它是怎麼做的。

package com.stay4it.testflowlayout;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * 原創作者:
 * 谷哥的小弟
 *
 * 博客地址:
 * http://blog.csdn.net/lfdfhl
 */
public class MyFlowLayout extends ViewGroup{
    private int  verticalSpacing = 20;
    public MyFlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int widthUsed = paddingLeft + paddingRight;
        int heightUsed = paddingTop + paddingBottom;

        int childMaxHeightOfThisLine = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                int childUsedWidth = 0;
                int childUsedHeight = 0;
                measureChild(child,widthMeasureSpec,heightMeasureSpec);
                childUsedWidth += child.getMeasuredWidth();
                childUsedHeight += child.getMeasuredHeight();

                LayoutParams childLayoutParams = child.getLayoutParams();

                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childLayoutParams;

                childUsedWidth += marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
                childUsedHeight += marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;

                if (widthUsed + childUsedWidth < widthSpecSize) {
                    widthUsed += childUsedWidth;
                    if (childUsedHeight > childMaxHeightOfThisLine) {
                        childMaxHeightOfThisLine = childUsedHeight;
                    }
                } else {
                    heightUsed += childMaxHeightOfThisLine + verticalSpacing;
                    widthUsed = paddingLeft + paddingRight + childUsedWidth;
                    childMaxHeightOfThisLine = childUsedHeight;
                }

            }

        }

        heightUsed += childMaxHeightOfThisLine;
        setMeasuredDimension(widthSpecSize, heightUsed);
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int childStartLayoutX = paddingLeft;
        int childStartLayoutY = paddingTop;

        int widthUsed = paddingLeft + paddingRight;

        int childMaxHeight = 0;

        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                int childNeededWidth, childNeedHeight;
                int left, top, right, bottom;

                int childMeasuredWidth = child.getMeasuredWidth();
                int childMeasuredHeight = child.getMeasuredHeight();

                LayoutParams childLayoutParams = child.getLayoutParams();
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childLayoutParams;
                int childLeftMargin = marginLayoutParams.leftMargin;
                int childTopMargin = marginLayoutParams.topMargin;
                int childRightMargin = marginLayoutParams.rightMargin;
                int childBottomMargin = marginLayoutParams.bottomMargin;
                childNeededWidth = childLeftMargin + childRightMargin + childMeasuredWidth;
                childNeedHeight = childTopMargin + childBottomMargin + childMeasuredHeight;

                if (widthUsed + childNeededWidth <= r - l) {
                    if (childNeedHeight > childMaxHeight) {
                        childMaxHeight = childNeedHeight;
                    }
                    left = childStartLayoutX + childLeftMargin;
                    top = childStartLayoutY + childTopMargin;
                    right = left + childMeasuredWidth;
                    bottom = top + childMeasuredHeight;
                    widthUsed += childNeededWidth;
                    childStartLayoutX += childNeededWidth;
                } else {
                    childStartLayoutY += childMaxHeight + verticalSpacing;
                    childStartLayoutX = paddingLeft;
                    widthUsed = paddingLeft + paddingRight;
                    left = childStartLayoutX + childLeftMargin;
                    top = childStartLayoutY + childTopMargin;
                    right = left + childMeasuredWidth;
                    bottom = top + childMeasuredHeight;
                    widthUsed += childNeededWidth;
                    childStartLayoutX += childNeededWidth;
                    childMaxHeight = childNeedHeight;
                }
                child.layout(left, top, right, bottom);
            }
        }
    }

}

現就該代碼中的主要操作做一些分析和介紹。

控件繼承自ViewGroup,請參見代碼第15行。
系統自帶的布局比如LinearLayout很難滿足標簽自動換行的功能,所以繼承ViewGroup實現自需的設計和邏輯

重寫onMeasure( ),請參見代碼第22-71行。
2.1 獲取View寬和高的mode和size,請參見代碼第23-26行。
此處widthSpecSize表示了View的寬,該值在判斷是否需要換行時會用到。
2.2 計算View在水平方向和垂直方向已經占用的大小,請參見代碼第33-34行。
在源碼階段也分析過這些已經占用的大小主要指的是View的padding值。
2.3 測量每個子View的寬和高,請參見代碼第38-67行。
這一步操作是關鍵。在這一步中需要測量出來每個子View的大小從而計算出該控件的高度。
在對代碼做具體分析之前,我們先明白幾個問題。
第一點:
我們常說測量每個子View的寬和高是為了將每個子View的寬累加起來得到父View的寬,將每個子View的高累加起來得到父View的高。
在此處,控件的寬就是屏幕的寬,所以我們不用去累加每個子View的寬,但是要利用子View的寬判斷換行的時機。
至於控件的高,還是需要將每個子View的高相累加。
第二點:
怎麼判斷需要換行顯示新的tag呢?如果:
這一行已占用的寬度+即將顯示的子View的寬度>該行總寬度
那麼就要考慮換行顯示該tag
第三點:
如果十個人站成一排,那麼這個隊伍的高度是由誰決定的呢?當然是這排人裡個子最高的人決定的。同樣的道理,幾個tag擺放在同一行,這一行的高度就是由最高的tag的值決定的;然後將每一行的高度相加就是View的總高了。

嗯哼,明白了這些,我們再看代碼就容易得多了。
第一步:
利用measureChild( )測量子View,請參見代碼第43行。
第二步:
計算子View需要占用的寬和高(childUsedWidth和childUsedHeight),請參見代碼第51-52行。
第三步:
判斷和處理是否需要換行,請參見代碼第54-63行。
第四步:
利用setMeasuredDimension()設置View的寬和高,請參見代碼第70行

重寫onLayout( ),請參見代碼第75-133行。
在onMeasure中已經對每個子View進行了測量,在該階段需要把每個子View擺放在合適的位置。
所以核心是確定每個子View的left, top, right, bottom。
在該過程中,同樣需要考慮換行的問題,思路也和measure階段類似,故不再贅述。

嗯哼,完成了該自定義控件的代碼,該怎麼樣使用呢?

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