Android 之 自定義控件 之 View

介紹:

Android的framework有大量的Views用來與用戶進行交互并顯示不同種類的數據,但是在實際開發(fā)中經常會遇到現有的UI控件不能滿足項目需求,或一個功能涉及到多個UI控件的組合,或實現某一特效的UI,這時必須通過自定義View的方式,實現這些功能。

方式:

1.多個控件組合在一起
2.繼承至目前已經提供的某個基礎的View
3.繼承至View或ViewGroup

View 和 ViewGroup的關系

View是基類,一般表示具體到某個控件,ViewGroup是它的子類,但又是layout的基類,一般作為一個視圖容器。由于ViewGroup是View的子類,所以View的方法基本都有,但是View的直接子類不具備ViewGroup的屬性。雖然他們是繼承關系,但是一般情況下我們可以看作是兩種形式,一種是控件,一種是容器。

View的三大核心方法onMeasure、onLayout、onDraw

onMeasure:用于測量視圖的大?。?br> onLayout:用于給視圖進行布局;
onDraw:用于對視圖進行繪制;

View重繪方法

invalidate():
當view的某些內容發(fā)生變化的時候,需要調用invalidate來通知系統對這個view進行重繪。
requestLayout():
當某些元素變化會引起組件大小變化時,需要調用requestLayout方法。

開始

1.繼承至View
為了讓Android Developer Tools能夠識別你的view,你必須至少提供一個構造方法,它包含一個Contenx與一個AttributeSet對象作為參數。這個狗雜哦方法允許布局編輯器創(chuàng)建并編輯你的view的實例。

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

2.自定義屬性
為了添加一個內置的View到你的UI上,你需要通過XML屬性來指定它的樣式與行為。良好的自定義views可以通過XML添加來改變樣式,為了讓你的自定義的view也有如此的行為,你應該:

為你的view在資源標簽下定義自設的屬性
在你的XML layout中指定屬性值
在運行時獲取屬性值
把獲取到的屬性值應用在你的view上

添加 資源文件到你的項目中。放置于res/values/attrs.xml文件中。

 <!--
    reference 參考某一資源ID
    dimension 尺寸值
    boolean:布爾值
    integer 整數型
    string  字符型
    color 顏色值
    ...等等

   名稱對應你所建立的View類名
    -->
    <declare-styleable name="MyView">
        <attr name="titleName" format="string|reference"/>
        <attr name="icon" format="reference"/>
        <attr name="titleColor" format="color"/>
        <attr name="count" format="integer"/>
        <attr name="titleSize" format="dimension"/>
        <attr name="typeface">
              <enum name="normal" value="0" />
              <enum name="sans" value="1" />
              <enum name="serif" value="2" />
              <enum name="monospace" value="3" />
         </attr>
    </declare-styleable>
    

一旦你定義了自設的屬性,你可以在layout XML文件中使用它們,就像內置屬性一樣。唯一不同的是你自設的屬性是歸屬于不同的命名空間。不是屬于http://schemas.android.com/apk/res/android 的命名空間,它們歸屬于http://schemas.android.com/apk/res/你的包名
或者 使用http://schemas.android.com/apk/res-auto

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
 <com.example.customviews.MyView
     custom:titleName="hello world"
     custom:titleColor="#333333"
     custom:typeface="normal" />
</LinearLayout>

當view從XML layout被創(chuàng)建的時候,在xml標簽下的屬性值都是從resource下讀取出來并傳遞到view的constructor作為一個AttributeSet參數。盡管可以從AttributeSet中直接讀取數值,可是這樣做有些弊端:

擁有屬性的資源并沒有經過解析
Styles并沒有運用上

所以我們需要通過obtainStyledAttributes()來獲取屬性值。這個方法會傳遞一個TypedArray對象。

//TypedArray是一個用來存放由context.obtainStyledAttributes獲得的屬性的數組
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
 try {
       CharSequence titlleName = a.getString(R.styleable.MyView_titleName, false);
       int count = a. getInteger(R.styleable.MyView_count, 0);
       int icon=a.getResourceId(R.styleable.MyView_icon, 0);
   } finally {
       a.recycle();
   }

TypedArray使用完成后一定要調用其recycle方法,否則會有內存泄露的問題

3.onDraw繪制
重繪一個自定義的view的最重要的步驟是重寫onDraw()方法。onDraw()的參數是一個Canvas對象。Canvas類定義了繪制文本,線條,圖像與許多其他圖形的方法。你可以在onDraw方法里面使用那些方法來創(chuàng)建你的UI。在你調用任何繪制方法之前,你需要創(chuàng)建一個Paint對象。

android.graphics framework把繪制定義為下面兩類:
繪制什么,由Canvas處理
如何繪制,由Paint處理

常見的操作:
繪制文字使用drawText()。指定字體通過調用setTypeface(), 通過setColor()來設置文字顏色.
繪制基本圖形使用drawRect(), drawOval(), drawArc(). 通過setStyle()來指定形狀是否需要filled, outlined.
繪制一些復雜的圖形,使用Path類. 通過給Path對象添加直線與曲線, 然后使用drawPath()來繪制圖形. 和基本圖形一樣,paths也可以通過setStyle來設置是outlined, filled, both.
通過創(chuàng)建LinearGradient對象來定義漸變。調用setShader()來使用LinearGradient。
通過使用drawBitmap來繪制圖片.
例:

/**
     * 
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //drawRect(RectF rect, Paint paint) //繪制區(qū)域,參數一為RectF一個區(qū)域
        RectF rect = new RectF(10,100,300,300);
        mPaint.setStyle(Paint.Style.FILL);  //填充
        canvas.drawRect(rect,mPaint);


        //drawPath(Path path, Paint paint) //繪制一個路徑,參數一為Path路徑對象
        Path path = new Path();
        path.moveTo(50,500);   //設置起始點
        path.lineTo(100,720); //連接點
        path.lineTo(400,420); //連接點
        path.close();
        mPaint.setStyle(Paint.Style.FILL);    //畫筆
        mPaint.setColor(Color.BLUE);
        canvas.drawPath(path,mPaint);


        //貼圖
        //參數一就是我們常規(guī)的Bitmap對象,
        //參數二是源區(qū)域(這里是bitmap)
        //參數三是目標區(qū)域(應該在canvas的位置和大小)
        //參數四是Paint畫刷對象
        //因為用到了縮放和拉伸的可能,當原始Rect不等于目標Rect時性能將會有大幅損失。
        //drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
        canvas.drawBitmap(mBitmap,500,500,mPaint);

        Rect src = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());  //想要繪制原圖的哪部分區(qū)域
        RectF dst = new RectF(200,200,500,800);  //繪制的位置
        canvas.drawBitmap(mBitmap,src,dst,mPaint);

         Matrix matrix = new Matrix();
        matrix.reset();

        //縮放
//        matrix.setScale(0.8f,0.8f);
        matrix.setRotate(30);
        canvas.drawBitmap(mBitmap,matrix,mPaint);


        //畫線,
        //參數一起始點的x軸位置,
        //參數二起始點的y軸位置,
        //參數三終點的x軸水平位置,
        //參數四y軸垂直位置,
        //最后一個參數為Paint 畫刷對象。
        //drawLine(float startX, float startY, float stopX, float stopY, Paintpaint)
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setStrokeWidth(20);  //設置線寬
        canvas.drawLine(50f,50f,200f,200f,mPaint);

        //畫點,參數一水平x軸,參數二垂直y軸,第三個參數為Paint對象。
        //drawPoint(float x, float y, Paint paint)
        canvas.drawPoint(500f,500f,mPaint);

        //渲染文本,Canvas類除了上面的還可以描繪文字,
        //參數一是String類型的文本,
        //參數二x軸,
        //參數三y軸,
        //參數四是Paint對象。
        //drawText(String text, float x, floaty, Paint paint)
        mPaint.setStrokeWidth(1);
        canvas.drawText("CD1605",800F,400F,mPaint);

        //方法,該方法可以沿著Path繪制文本
        // 其中hOffset參數指定水平偏移 (文本間距)
        // vOffset參數指定垂直偏移(距離線頂部的間距)
        canvas.drawTextOnPath("天行健,君子以自強不息",path,-20,80,mPaint);

        //畫橢圓,
        //參數一是掃描區(qū)域 即 橢圓區(qū)域
        //參數二為paint對象;
        //drawOval(RectF oval, Paint paint)
        RectF oval = new RectF(300f,800f,600f,900f);
        canvas.drawOval(oval,mPaint);
        //API>=21
//        canvas.drawOval(200f,500f,600f,800f,mPaint);


        // 繪制圓,
        // 參數一是中心點的x軸,
        // 參數二是中心點的y軸,
        // 參數三是半徑,
        // 參數四是paint對象;
        //drawCircle(float cx, float cy, float radius,Paint paint)
        mPaint.setARGB(255,100,255,255);
        canvas.drawCircle(700,700,100,mPaint);

        //畫弧
        //參數一是RectF對象,一個矩形區(qū)域橢圓形的界限用于定義在形狀、大小、弧,
        //參數二是起始角(度)在電弧的開始,
        //參數三掃描角(度)開始順時針測量的,
        //參數四是如果這是真的話,包括橢圓中心的電弧,并關閉它,如果它是假這將是一個弧線,
        //參數五是Paint對象;
        //drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
        RectF arc = new RectF(300f,1200f,600f,1400f);
        mPaint.setARGB(255,100,100,255);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawArc(arc,0,180,false,mPaint);




        // 線性渲染
        // 其中,參數x0表示漸變的起始點x坐標;
        // 參數y0表示漸變的起始點y坐標;
        // 參數x1表示漸變的終點x坐標;
        // 參數y1表示漸變的終點y坐標??;
        // color0表示漸變開始顏色;
        // color1表示漸變結束顏色;
        // 參數tile表示平鋪方式。
        Shader mShader = new LinearGradient(
                0, 0, 100, 100,
                new int[] { Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW,
                        Color.LTGRAY }, null, Shader.TileMode.REPEAT); // 一個材質,打造出一個線性梯度沿著一條線。

        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setShader(mShader);
        RectF oval2 = new RectF(60, 100, 200, 240);// 設置個新的長方形,掃描測量
        canvas.drawArc(oval2, 200, 130, true, mPaint);
        // 畫弧,第一個參數是RectF:該類是第二個參數是角度的開始,第三個參數是多少度,第四個參數是真的時候畫扇形,是假的時候畫弧線


        //畫貝塞爾曲線
        mPaint.reset();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.GREEN);
        mPaint.setStrokeWidth(20);
        Path path2=new Path();
        path2.moveTo(100, 320);//設置Path的起點

        // x1,y1為控制點的坐標值,x2,y2為終點的坐標值;
        // 貝塞爾曲線的形成,就比如我們把一條橡皮筋拉直,橡皮筋的頭尾部對應起點和終點,
        // 然后從拉直的橡皮筋中選擇任意一點(除頭尾對應的點外)扯動橡皮筋形成的彎曲形狀,
        // 而那個扯動橡皮筋的點就是控制點

        path2.quadTo(300, 310, 170, 400); //設置貝塞爾曲線的控制點坐標和終點坐標
//        path2.close();//閉合繪制
        canvas.drawPath(path2, mPaint);//畫出貝塞爾曲線

        //繪制 畫布 
        //canvas.drawColor(Color.YELLOW);
        //清空 或者 
        //canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    }

**4.onMeasure **
為了正確的繪制你的view,你需要知道view的大小。復雜的自定義view通常需要根據在屏幕上的大小與形狀執(zhí)行多次layout計算。而不是假設這個view在屏幕上的顯示大小。即使只有一個程序會使用你的view,仍然是需要處理屏幕大小不同,密度不同,方向不同所帶來的影響。
盡管view有許多方法是用來計算大小的,但是大多數是不需要重寫的。如果你的view不需要特別的控制它的大小,唯一需要重寫的方法是[onSizeChanged()](http://developer.android.com/reference/android/view/View.html#onSizeChanged(int, int, int, int)).
onSizeChanged(),當你的view第一次被賦予一個大小時,或者你的view大小被更改時會被執(zhí)行。在onSizeChanged方法里面計算位置,間距等其他與你的view大小值。
當你的view被設置大小時,layout manager(布局管理器)假定這個大小包括所有的view的內邊距(padding)。當你計算你的view大小時,你必須處理內邊距的值。這段MyView.onSizeChanged()
代碼演示:

     // Account for padding
       float xpad = (float)(getPaddingLeft() + getPaddingRight());
       float ypad = (float)(getPaddingTop() + getPaddingBottom());

       // Account for the label
       if (mShowText) xpad += mTextWidth;

       float ww = (float)w - xpad;
       float hh = (float)h - ypad;

       // Figure out how big we can make the pie.
       float diameter = Math.min(ww, hh);

如果你想更加精確的控制你的view的大小,需要重寫[onMeasure()](http://developer.android.com/reference/android/view/View.html#onMeasure(int, int))方法。這個方法的參數是View.MeasureSpec,它會告訴你的view的父控件的大小。那些值被包裝成int類型,你可以使用靜態(tài)方法來獲取其中的信息。

/**
     * 布局測量
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //MeasureSpec通常翻譯為”測量規(guī)格”,它是一個32位的int數據.
        //其中高2位代表SpecMode即某種測量模式,低30位為SpecSize代表在該模式下的規(guī)格大小.
        //getMode 獲取設置的模式AT_MOST、EXACTLY、UNSPECIFIED
    
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width=0,height=0;

        if(widthMode ==MeasureSpec.AT_MOST){
            //寬度類型為warp_content ,
            //此處如果布局設置為 warp_content我們將width設置為200PX(這個高度應該是你自己繪制的區(qū)域高度)
            width = 200;
        }else if(widthMode ==MeasureSpec.EXACTLY){
            //寬度類型為match_parent 或者定義好的dp值得,此處我們設置為控件測量獲取的父容器所給予的寬度
            width = widthSize;
        }else if(widthMode ==MeasureSpec.UNSPECIFIED){
            //表示子布局想要多大就多大,一般出現在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此種模式比較少見,一般用不到
        }

        if(heightMode ==MeasureSpec.AT_MOST){
            //高度類型為warp_content
            //此處如果布局設置為 warp_content我們將height設置為200PX(這個高度應該是你自己繪制的區(qū)域高度)
            height = 200;
        }else if(heightMode ==MeasureSpec.EXACTLY){
            //高度類型為match_parent 或者定義好的dp值得,此處我們設置為控件測量獲取的父容器所給予的高度
            height = heightSize;
        }else if(heightMode ==MeasureSpec.UNSPECIFIED){
            //表示子布局想要多大就多大,一般出現在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此種模式比較少見,一般用不到
        }

        //設置控件寬高
        setMeasuredDimension(width,height);
    }

上面的代碼有2個重要的事情需要注意:

1.計算的過程有把view的最好把padding考慮進去。這部分是view所控制的。(此處的示例代碼 沒有給出)
2.onMeasure()沒有返回值。它通過調用setMeasuredDimension()來獲取結果。調用這個方法是強制執(zhí)行的,如果你遺漏了這個方法,會出現運行時異常。

4.onTouchEvent給你的view添加觸摸動作
有些時候我們需要給我們的View添加一些手指按下,移動,抬起的操作,這時候就需要重寫 View 的 onTouchEvent方法。

/**
     * 事件觸發(fā)
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG,"按下:("+event.getX()+":"+event.getY()+")");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG,"移動:("+event.getX()+":"+event.getY()+")");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG,"抬起:("+event.getX()+":"+event.getY()+")");
                break;
        }
        //消費事件
        return true;
    }

很多時候onTouch本身并不能滿足我們的需求,比如快速滑動View時的速度等,這時候就需要借助另外一個Android給我們提供的另一個touch事件GestureDetector

class mListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
mDetector = new GestureDetector(PieChart.this.getContext(), new ```
不管你是否使用GestureDetector.SimpleOnGestureListener, 你必須總是實現onDown()方法,并返回true。這一步是必須的,因為所有的gestures都是從onDown()開始的。如果你在onDown()里面返回false,系統會認為你想要忽略后續(xù)的gesture,那么GestureDetector.OnGestureListener的其他回調方法就不會被執(zhí)行到了。一旦你實現了GestureDetector.OnGestureListener并且創(chuàng)建了GestureDetector的實例, 你可以使用你的GestureDetector來中止你在onTouchEvent里面收到的touch事件。

Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mDetector.onTouchEvent(event);
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
stopScrolling();
result = true;
}
}
return result;
}

當你傳遞一個touch事件到onTouchEvent()時,若這個事件沒有被辨認出是何種gesture,它會返回false。你可以執(zhí)行自定義的gesture-decection代碼。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容