Android 淺談自定義View(2)

之前我們從源碼的角度對View的工作流程進行了分析仑荐,有了這些理論的支撐,我們才能讓自定義View更好的服務于我們的工作挑宠,接下來我們聊聊自定義View中的那些“套路”均驶。如果還不了解View的工作流程,可以先閱讀這篇文章:Android 淺談自定義View(1)竿屹。

根據(jù)自定義View的使用場景和自定義View的繼承關系报强,我們可以將自定義View分四類:

  • 1、繼承系統(tǒng)的View類
  • 2拱燃、繼承特定的View類(例如TextView秉溉、ProgressBar等)
  • 3、繼承系統(tǒng)的ViewGroup類
  • 4、繼承特定的ViewGroup類(例如LinearLayout坚嗜、RelativeLayout等)

四種類型的自定義View有什么不同夯膀、各自的特點是什么呢,以及如何選擇選擇一種合適的方式來實現(xiàn)自定義View苍蔬,這些應該是我們關心的點诱建。接下來,我們結合具體的場景逐一的分析下四種類型的自定義View碟绑。

一俺猿、繼承系統(tǒng)的View類

這種類型的自定義View多用來實現(xiàn)一些不規(guī)則的效果,同時不需要包含子View格仲,而且我們無法通過擴展已有的控件來實現(xiàn)押袍,因為是直接繼承系統(tǒng)的View類,所以我們應在onMeasure()方法中對View的尺寸進行重新的測量來支持wrap_content屬性凯肋,否則View使用wrap_content屬性將和使用match_parent屬性是一個效果谊惭,當然這并不是我們愿意看到的,原因在上一篇文章中已經分析過了侮东,同時這種情況下圈盔,如果View使用了padding屬性,我們依然無法看到效果悄雅,所以需要在onDraw()方法中對padding屬性進行支持驱敲,考慮到了這些因素,我們的自定義View才能更加的健壯宽闲。一般情況下众眨,這種類型的自定義View需要在onDraw()方法中通過canvas繪制的方式來實現(xiàn)具體的效果。

來看一個例子容诬,我們在簡單的在onDraw()方法中設置View背景為灰色娩梨,并繪制了一個圓:

public class CircleView extends View {
    private Paint mPaint;
    public CircleView(Context context) {
        this(context, null);
    }
    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.FILL);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width / 2, height / 2);
        canvas.drawColor(Color.GRAY);//設置灰色背景
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);//繪制圓形
    }
}

在布局文件中這樣使用:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.viewdemo.CircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp" />
</RelativeLayout>

看下最終的效果:


CircleView1

和我們分析的一樣,由于沒有支持wrap_content和padding屬性览徒,我們的自定義View和match_parent的效果一樣狈定,而且設置的padding屬性無效。接下來繼續(xù)完善:

public class CircleView extends View {
    .......省略若干代碼........    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(500, 500);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(500, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 500);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min((width - getPaddingLeft() - getPaddingRight()) / 2,
                (height - getPaddingTop() - getPaddingBottom()) / 2);
        canvas.drawColor(Color.GRAY);
        canvas.drawCircle(width / 2, height / 2, radius, mPaint);
    }
}

在onMesure()方法中吱殉,如果寬/高的測量模式為MeasureSpec.AT_MOST掸冤,我們通過setMeasuredDimension()重新測量View的尺寸,這樣就解決了使用wrap_content屬相帶來的問題友雳,同時在onDraw()方法中計算半徑時考慮padding屬性稿湿。再看下最終的效果:

CircleView2

此時View的寬/高為500px,同時padding屬性也生效了押赊。其它情況大家可以自行測試哦饺藤。

二包斑、繼承特定的View類

這種類型的自定義View相對第一種要簡單一些,因為我們直接繼承特定的View類涕俗,例如TextView罗丰、ImageView等,這些系統(tǒng)已經對這些View類進行了很好的實現(xiàn)再姑,所以一般情況下我們不需要對wrap_content萌抵、padding屬相進行特別的支持。如果我們要實現(xiàn)的自定義View和系統(tǒng)已有的某個View類似元镀,可以考慮這種方式绍填,我們只需要對其進行擴展即可。和第一種類型類似栖疑,這種自定義View一般也需要在onDraw()方法中通過canvas繪制的方式來實現(xiàn)具體的效果讨永。例如我們要實現(xiàn)一個圓角的TextView就可以采用這種方式:

public class RoundTextView extends TextView {
    private Paint mPaint;
    public RoundTextView(Context context) {
        super(context);
        init();
    }
    public RoundTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init(){
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
    }
   //重寫setBackgroundColor()來設置畫筆顏色
    @Override
    public void setBackgroundColor(int color) {
        mPaint.setColor(color);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        RectF rect = new RectF(0, 0, getWidth(), getHeight());
        canvas.drawRoundRect(rect, 10, 10, mPaint);//繪制圓角矩形作為TextView背景
        super.onDraw(canvas);
    }
}

在布局文件中的使用方法和系統(tǒng)的TextView一樣,有一點需要注意遇革,如果要設置背景色卿闹,則要通過java代碼:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RoundTextView roundTextView = (RoundTextView) findViewById(R.id.round_tv);
        roundTextView.setBackgroundColor(Color.RED);//設置背景為紅色
    }
}

最后看一下效果:


RoundTextView

簡單的擴展就實現(xiàn)了圓角的效果,不需要額外的drawable背景或者圖片萝快。

三锻霎、繼承系統(tǒng)的ViewGroup類

我們知道系統(tǒng)已經提供了LinearLayout、RelativeLayout這樣的ViewGroup實現(xiàn)類杠巡,但畢竟這些布局控件的都有其特定的使用場景量窘,如果我們需要若干個View按照某種規(guī)則組合在一起雇寇,而系統(tǒng)的布局控件無法實現(xiàn)類似的場景氢拥,我們可以考慮采用這種方式來定義一種新的布局控件。但需要注意的是锨侯,在內容區(qū)域未超過屏幕尺寸的情況下嫩海,我們一般需要在onMeasure()中重新測量ViewGroup尺寸來對wrap_content屬性進行支持,如果內容區(qū)域的大小超過屏幕尺寸囚痴,我們就必須在onMeasure()中重新測量ViewGroup的尺寸叁怪,否則ViewGroup的最大尺寸為屏幕尺寸,導致ViewGroup中的內容顯示不全深滚。同時根據(jù)需要還可以考慮自身的padding屬性以及子View的margin屬性奕谭,這些都會影響我們自定義View最終的測量結果,通常需要在onLayout()方法中確定子View的具體位置痴荐。解析來看一個具體的例子:

public class TestViewGroup extends ViewGroup {
    //使ViewGroup支持margin屬性
    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        int width = 0;
        int height = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
            width += childView.getMeasuredWidth() + params.rightMargin + params.leftMargin;

            if (i == 0) {
                height += childView.getMeasuredHeight() + params.topMargin + params.bottomMargin;
            }
        }
        if (width > getScreenWidth()) {
            setMeasuredDimension(width, height);
        } else {
            setMeasuredDimension((widthSpecMode == MeasureSpec.AT_MOST) ? width : widthSpecSize,
                    (heightSpecMode == MeasureSpec.AT_MOST) ? height : heightSpecSize);
        }
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = 0;
        View lastChildView = null;
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
            left += params.leftMargin;
            if (lastChildView != null) {
                left += lastChildView.getMeasuredWidth() + ((MarginLayoutParams) lastChildView.getLayoutParams()).rightMargin;
            }
            int right = left + childView.getMeasuredWidth();
            int top = params.topMargin;
            int bottom = childView.getMeasuredHeight() + top;
            childView.layout(left, top, right, bottom);
            lastChildView = childView;
        }
}

省略了一些非核心代碼血柳,首先通過重寫generateLayoutParams()方法使ViewGroup支持margin屬性,在onMeasure()中生兆,如果計算出子View的總寬度大于屏幕寬度难捌,則根據(jù)子View尺寸直接重新測量ViewGroup尺寸,否則使用系統(tǒng)默認的測量值,只在ViewGroup布局參數(shù)為wrap_content時使用子View的計算尺寸重新測量ViewGroup尺寸根吁。由于我們實現(xiàn)了一個類似水平滾動的ViewGroup员淫,所以在onLayout()中按照水平從左到右的方式確定View的位置。同時我們考慮了margin屬性击敌,所以子View可以使用margin屬性介返。看一下效果:

TestViewGroup

四沃斤、繼承特定的ViewGroup類

如果我們的自定View是若干個View組合在一起的效果映皆,同時在系統(tǒng)已有的布局控件中可以找到類似的效果,則可以考慮繼承特定的ViewGroup類轰枝,例如LinearLayout捅彻、RelativeLayout等,比如我們在界面中通常需要頂部title鞍陨,就可以考慮直接繼承LinearLayout來進行封裝步淹,來方便復用。當然通過直接繼承ViewGroup類也可以實現(xiàn)诚撵,但是難度會增加很多缭裆,得不償失。舉個例子吧寿烟,當LinearLayout為垂直方向澈驼,且其中的內容超過屏幕的顯示范圍,則因為LinearLayout的內容區(qū)域無法滾動筛武,我們無法預覽整個LinearLayout內容缝其,有一只解決辦法是通過和ScrollView嵌套。那能不能擴展LinearLayout來實現(xiàn)呢徘六,繼續(xù)往下看:

public class ScrollLinearLayout extends LinearLayout {
    private int mLastY;
    private Context mContext;
    //計算ScrollLinearLayout在屏幕的最大顯示高度
    private int showHeight;

    public ScrollLinearLayout(Context context) {
        this(context, null);
    }
    public ScrollLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        setClickable(true);//使onTouchEvent()方法可以消費事件
        showHeight = getScreenHeight() - getStatusBarHeight() - getActionBarHeight();
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
       //計算ScrollLinearLayout子View高度
        int height = 0;
        for (int i = 0 ; i < getChildCount(); i++){
            height += getChildAt(i).getMeasuredHeight();
        }
        if (height > showHeight){
            setMeasuredDimension(widthMeasureSpec, height);
        }
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(y - mLastY) > mTouchSlop) {
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastY = y;
        return intercepted;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                 if (getHeight() < showHeight){
                    return true;
                }
                int scrollY = getScrollY();
                int dy = mLastY - y;
                if (scrollY + dy <= 0) {
                    scrollTo(0, 0);
                    return true;
                } else if (scrollY + dy >= getHeight() - showHeight) {
                    scrollTo(0, getHeight() - showHeight);
                    return true;
                }
                scrollBy(0, dy);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastY = y;
        return super.onTouchEvent(event);
    }
...........省略若干行代碼...........
}

核心代碼很簡單内边,在onMeasure()方法中計算ScrollLinearLayout 的高度,如果子View高度總和大于其在屏幕的最大顯示高度待锈,則重新測量其尺寸漠其。在onTouchEvent()中使ScrollLinearLayout的內容跟隨手指移動,同時進行邊界檢測竿音,防止超出屏幕范圍和屎。最后看下效果:


ScrollLinearLayout

到這里常見的自定義View類型就介紹完畢了,難免有疏忽的地方春瞬,還請指正柴信,自定義View大致流程上有一定的規(guī)律可循,但更多的方法經驗還需要在實踐中總結快鱼。

有興趣的話颠印,可以下載源碼看看:點我下載哦...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末纲岭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子线罕,更是在濱河造成了極大的恐慌止潮,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钞楼,死亡現(xiàn)場離奇詭異喇闸,居然都是意外死亡,警方通過查閱死者的電腦和手機询件,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進店門燃乍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宛琅,你說我怎么就攤上這事刻蟹。” “怎么了嘿辟?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵舆瘪,是天一觀的道長。 經常有香客問我红伦,道長英古,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任昙读,我火速辦了婚禮召调,結果婚禮上,老公的妹妹穿的比我還像新娘蛮浑。我一直安慰自己唠叛,他們只是感情好,可當我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布陵吸。 她就那樣靜靜地躺著玻墅,像睡著了一般介牙。 火紅的嫁衣襯著肌膚如雪壮虫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天环础,我揣著相機與錄音囚似,去河邊找鬼。 笑死线得,一個胖子當著我的面吹牛饶唤,可吹牛的內容都是我干的。 我是一名探鬼主播贯钩,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼募狂,長吁一口氣:“原來是場噩夢啊……” “哼办素!你這毒婦竟也來了?” 一聲冷哼從身側響起祸穷,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤性穿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后雷滚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體需曾,經...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年祈远,在試婚紗的時候發(fā)現(xiàn)自己被綠了呆万。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡车份,死狀恐怖谋减,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情扫沼,我是刑警寧澤逃顶,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站充甚,受9級特大地震影響以政,放射性物質發(fā)生泄漏。R本人自食惡果不足惜伴找,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一盈蛮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧技矮,春花似錦抖誉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至樊零,卻和暖如春我磁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驻襟。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工夺艰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沉衣。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓郁副,卻偏偏與公主長得像,于是被迫代替她去往敵國和親豌习。 傳聞我的和親對象是個殘疾皇子存谎,可洞房花燭夜當晚...
    茶點故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內容