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的效果已經達到了。
如果覺得我的文章對你有用,請留言鼓勵一下吧!