Android自定義View講解加示例


Android自定義View是工程開發中必不可少的一項技能,項目中通過自定義View的方式造好各種內部需要的View,將會帶來極大的使用方便。


一、自定義View的幾種使用方式

(1)自繪控件:使用canvas畫出控件的樣子

(2)組合一些Android的控件:通過繼承容器,將一些現有的組件組合起來成爲一個固定的View

(3)繼承並擴展Android的控件:對原有的Android View進行擴展,在原有功能上添加新的功能。

相對來說,組合和繼承比較容易,自繪控件要稍微複雜一點。本次將主要講解自繪控件的方式,並通過一個具體的例子,展示怎麼畫出一個控件、怎麼自定義屬性來控制控件的樣式、併爲控件添加一些行爲特徵。


二、使用自定義View,一般需重寫以下4個方法,但不是必須都重寫,根據需要選擇性重寫即可。

onMeasure():測量控件本身的大小
onLayout():測量控件在父控件中的位置
onDraw():構建了自定義View的外觀形象
onTouchEvent():重載視圖的行爲</span>


三、示範製作一個自定View

本例子將製作一個自定義View,樣式爲一張小火箭圖片,xml中可以通過自定義屬性控制其樣式,點擊小火箭將開始一個翻轉動畫。好了,開始上代碼。

1. 設計自定義屬性

首先在res/values/  下建立一個attrs.xml , 在裏面定義我們的屬性和聲明我們的整個樣式。

 <?xml version="1.0" encoding="utf-8"?>
          <resources>
             <declare-styleable name="MyTouchBall">
                 <attr name="imgSampleSize" format="integer" />
                 <attr name="duration" format="integer" />
             </declare-styleable>
         </resources></span></span>


2.新建一個類MyTouchBall,繼承View,佈局文件中像用Android自帶控件一樣使用自定義View,注意引入命名控件(代碼第4行),通過命名空間方便使用自定義屬性

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:mytouchball="http://schemas.android.com/apk/res/com.example.administrator.myapplication"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.administrator.myapplication.MainActivity"
    >
    <com.example.administrator.myapplication.MyTouchBall
        android:layout_width="500dp"
        android:layout_height="wrap_content"
        mytouchball:duration="1000"
        mytouchball:imgSampleSize="1"
        >
    </com.example.administrator.myapplication.MyTouchBall>
</RelativeLayout></span></span>

3. 自定義View,這裏會有三個默認的構造方法,android開發者網站上有相關的說明文檔: 

public View (Context context)是在java代碼創建視圖的時候被調用,如從xml填充的視圖,就不會調用這個 。
public View (Context context, AttributeSet attrs)這個是在xml創建但是沒有指定style的時候被調用 
public View (Context context, AttributeSet attrs, int defStyle)給View提供一個基本的style</span>

我們在兩個參數的構造方法中,添加邏輯,接受xml佈局中的自定義屬性。

public class MyTouchBall extends View {

    private int mImgSampleSize = 10;  //火箭縮放比例
    private int mDuration = 500;  //火箭動畫的時間

    private Bitmap mBitmap;
    private int mImgHeight; //縮放後,圖片的實際高度
    private int mImgWidth;  //縮放後,圖片的實際寬度

    private Paint mPaint; //畫筆


    public MyTouchBall(Context context) {
        super(context);
    }

    //在這裏獲取自定義屬性,並賦值給成員變量
    public MyTouchBall(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTouchBall);
        int count = typedArray.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.MyTouchBall_duration:
                    mDuration = typedArray.getInteger(attr, 500);
                    break;
                case R.styleable.MyTouchBall_imgSampleSize:
                    mImgSampleSize = typedArray.getInteger(attr, 30);
                    break;
                default:
                    break;
            }
        }
        setBackgroundColor(getResources().getColor(R.color.sandybrown));//設置背景顏色爲淺黃色,方便識別onMeasure計算的對不對
        typedArray.recycle();

        //根據屬性計算火箭圖片的寬高
        mBitmap = decodeSampledBitmap(getResources(), R.mipmap.roket, mImgSampleSize);
        mImgWidth = mBitmap.getWidth();
        mImgHeight = mBitmap.getHeight();
    }

    public MyTouchBall(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }  }
</span></span>

decodeSampledBitmap方法是根據縮放比例,壓縮或放大圖片,下面是它的代碼。

<span style="font-family:SimSun;"><span style="font-family:SimSun;font-size:14px;">  //加載圖片,按比例縮放
    private static Bitmap decodeSampledBitmap(Resources res, int resId, int sampleSize) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
        options.inSampleSize = sampleSize;
        // 使用獲取到的inSampleSize值解析圖片
        return BitmapFactory.decodeResource(res, resId, options);
    }</span></span>

4. 重寫onMeasure方法,根據xml屬性來決定控件的寬高。

  如下問代碼所示,一般自定義控件都會重寫View的onMeasure方法,因爲該方法指定該控件在屏幕上的大小。
  

  onMeasure(int widthMeasureSpec, int heightMeasureSpec)傳入的兩個參數是由上一層控件傳入的大小,有多種情況,重寫該方法時需要對計算控件的實際大小,然後調用setMeasuredDimension(int, int)設置實際大小。

  onMeasure傳入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸數值,而是將模式和尺寸組合在一起的數值。

  通過int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式,用int size =?MeasureSpec.getSize(widthMeasureSpec)得到尺寸。

  其中,mode共有三種情況,取值分別爲MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。

  MeasureSpec.EXACTLY是精確尺寸,當我們將自定義View的layout_width或layout_height指定爲具體數值時如andorid:layout_width="50dip",或者match_parent就會是這個模式。(match_parent也相當於指定了具體尺寸)

  MeasureSpec.AT_MOST是最大尺寸,當控件的layout_width或layout_height指定爲WRAP_CONTENT時,控件大小一般隨着控件的子空間或內容進行變化,此時控件尺寸只要不超過父控件允許的最大尺寸即可。因此,此時的mode是AT_MOST,size給出了父控件允許的最大尺寸。

  MeasureSpec.UNSPECIFIED是未指定尺寸,這種情況不多.

 

  簡單總結,映射關係是:
   wrap_parent -> MeasureSpec.AT_MOST
   具體數值或者match_parent -> MeasureSpec.EXACTLY

<span style="font-family:SimSun;"><span style="font-family:SimSun;font-size:14px;">    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int width, height;

        if (widthMode == MeasureSpec.EXACTLY) { //使用者在佈局文件裏指定了具體的寬度
            width = widthSize;
        } else {  //沒有指定,或者設置成了wrap_content
            //這時候要計算具體的數值,寬度取圖片的寬度
            width = mImgWidth;
        }

        if (heightMode == MeasureSpec.EXACTLY) { //使用者在佈局文件裏指定了具體的高度
            height = heightSize;
        } else {  //沒有指定,或者設置成了wrap_content
            //寬度取圖片的高度
            height = mImgHeight;
        }
        setMeasuredDimension(width, height);
    }</span></span>

5. 重寫onDraw方法,畫出具體的界面,這裏就是將一張圖片畫到界面上(當然,你可以根據需要畫各種形狀)。

<span style="font-family:SimSun;"><span style="font-family:SimSun;font-size:14px;">    @Override
    protected void onDraw(Canvas canvas) {
        mPaint = new Paint();
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);//在畫布上畫出圖片
    }</span></span>

6. 重寫onTouchEvent方法,爲控件添加一些獨特的行爲,默認返回值用false,以便事件能回傳到上層,不影響原有功能。具體原因請看我的另一篇博客Android事件傳遞機制詳解(嵌套自定義View示例)

<span style="font-family:SimSun;"><span style="font-family:SimSun;font-size:14px;">    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            startAnimation();
        }
        return false;
    }</span></span>

7. 一個簡單的旋轉動畫效果

<span style="font-family:SimSun;"><span style="font-family:SimSun;font-size:14px;">  //開始動畫
    public void startAnimation() {
        ObjectAnimator.ofFloat(this, "rotationX", 0.0f, 360.0f).setDuration(mDuration).start();
    }</span></span>

三、結果

好了,大功告成,通過結果來看看效果吧。從之前的佈局文件可以看到,因爲寬度設置了500dp,整個控件的大小是500dp(通過背景色可以看出來);而高度設置的是wrap_content,所以控件的高度就是圖片的高度。這表明onMeasure的效果已經達到了。



如果覺得我的文章對你有用,請留言鼓勵一下吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章