自定義評分控件實現

評分控件在開發中算是使用率比較高的組件,Android自身也包含默認的評分控件,不過自帶的評分控件可定製性並不高,現在就通過自定義View的方式來實現簡單評分控件。自定義的評分控件繼承自View類型,它需要覆蓋View的三個構造函數:只帶有Context類型的構造函數通常是開發者在代碼中直接new創建,帶有Context和AttributeSet的構造函數在LayoutInflater從XML中創建控件使用,其中AttributeSet就包含了開發者在XML裏爲控件指定的各種屬性值,還有一個帶有int defStyleAttr的構造函數在XML中開發者指定了style屬性會被調用,沒有指定的默認值爲零代表使用默認的樣式。

// RatingBar構造函數代碼
public RatingBar(Context context) { // 直接new使用的構造函數
    this(context, null); // 調用兩個參數構造函數
}

public RatingBar(Context context, AttributeSet attrs) { // LayoutInflater創建時傳入XML配置
    this(context, attrs, 0); // 調用三參數構造函數
}
// LayoutInflater創建時傳入XML配置和style配置
public RatingBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initAttributes(context, attrs);
    init();
}

爲了方便開發者使用控件的屬性需要支持XML文件配置屬性,也要支持代碼直接配置控件的屬性值。XML文件的屬性需要在attrs.xml文件中定義屬性名稱和類型,在包含AttributeSet參數的構造函數中解析出定義的屬性值。評分控件的屬性值包括有評分時展示的圖標圖片,沒有評分時展示的圖標圖片,評分控件的最大值,評分控件當前值和評分控件星星圖片的間距。

<declare-styleable name="RatingBar">
     <attr name="emptyImage" format="reference" />
     <attr name="fullImage" format="reference" />
     <attr name="maxValue" format="integer" />
     <attr name="value" format="float" />
     <attr name="starPadding" format="dimension" />
</declare-styleable>

解析XML屬性值需要使用Context.obtainStyledAttributes()方法,它會從AttributeSet中解析出attrs.xml文件中定義的RatingBar內所有的屬性值並封裝到TypedArray對象中,注意TypedArray對象內部也使用了享元模式,獲取到對象後一定要記得及時地回收,要獲取屬性值只需要調用TypedArray對應類型的方法並且傳遞該屬性定義時的索引值就可以了。

// RatingBar解析XML文件配置屬性
private void initAttributes(Context context, AttributeSet attrs) {
  	if (attrs != null) { // 在XML中定義的屬性解析
       TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RatingBar);
       Drawable emptyDrawable = array.getDrawable(R.styleable.RatingBar_emptyImage);
       if (emptyDrawable instanceof BitmapDrawable) {
            mEmptyBitmap = ((BitmapDrawable) emptyDrawable).getBitmap();
       }
       Drawable fullDrawable = array.getDrawable(R.styleable.RatingBar_fullImage);
       if (fullDrawable instanceof BitmapDrawable) {
             mFullBitmap = ((BitmapDrawable) fullDrawable).getBitmap();
        }
        mStarPadding = array.getDimensionPixelSize(R.styleable.RatingBar_starPadding, 
mStarPadding);
        mValue = array.getFloat(R.styleable.RatingBar_value, mValue);
        mMaxValue = array.getInt(R.styleable.RatingBar_maxValue, mMaxValue);
        array.recycle();
    }
}

XML屬性設置解析完成後還需要爲直接代碼設置,通常情況下修改了控件屬性都需要及時刷新界面確保數據和展示保持一致,如果改變的屬性與控件大小或位置有關係就需要調用requestLayout()申請重新佈局和繪製,如果只與控件的展示內容有關係就只需要調用invalidate()把當前展示界面設置爲非法要求控件重新繪製即可。在評分控件中最常用的就是設置當前的評分值,評分只與界面的展示相關,只需要調用invalidate()要求重新繪製就可以了。

// RatingBar設置評分值代碼
public void setValue(float value) {
    if (value < 0) {
       mValue = 0;
    }

    if (value > mMaxValue) {
        mValue = mMaxValue;
    }
    mValue = value;
    Log.e(TAG, "value = " + mValue);
    invalidate(); // 重新繪製評論條界面
}

屬性值處理完後就要考慮控件的尺寸問題,measure()方法內部會做一些通用的工作,控件的實際測量工作通常都是在onMeasure()方法中進行的,它包含有兩個參數widthMeasureSpec和heightMeasureSpec,它們雖然都是int類型但裏面卻包含了兩種數據,前2個比特代表父控件提供的測量類型值,後面30個比特代表父控件提供的實際尺寸值。

// RatingBar測量展示尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    if (heightMode == MeasureSpec.EXACTLY) {
        if (height < DEFAULT_MIN_HEIGHT) {
            height = DEFAULT_MIN_HEIGHT;
        }
    } else {
        height = DEFAULT_MIN_HEIGHT;
    }

    int width = height * 5 + 4 * mStarPadding;
    setMeasuredDimension(width, height); // 保存下測量值
}

MeasureSpec類是專門用來處理這類數據的封裝類,MeasureSpec.getMode()獲取前兩個比特內的測量類型,主要包含三種類型MeasureSpec.EXACTLY精確測量值,MeasureSpec.AT_MOST控件尺寸最大值,MeasureSpec.UNSPECIFIED未指定值,需要控件自己決定需要多大的尺寸值。onMeasure()方法傳遞進來的測量規格參數是父控件根據自己的測量結果和子控件設置的LayoutParams佈局參數共同確定下來的參考值,它只是提供參考並不代表一定要按照測量規格參數的值來設置控件尺寸,真正測量值在調用setMeasuredDimension()方法後纔會真正的生效。在評分控件的測量中如果要求控件展示精確的高度而且不小於最小高度就使用精確值,如果要求AT_MOST或者UNSPECIFIED就使用默認高度值。評分控件內部的星星是正方形,高度值獲取到了就知道星星的寬度值,評分控件中5個星星在加上它們中間的間隔補白就是控件的寬度值。

在構造函數中會調用初始init()方法,它內部主要負責初始化繪製相關的對象,想要在控件內部繪製圖像就需要使用Paint畫筆對象,設置畫筆的抗鋸齒和防抖動功能使得繪製出來的圖片過度更加平滑不會出現很邊緣鋸齒和過度突兀的問題。由於onDraw()繪製方法會多次調用,在頻繁調用的方法內部如果創建本地對象,會產生大量的垃圾對象導致GC操作頻繁影響性能。因此onDraw() 方法中的局部對象都可以緩存在控件的屬性中。

// RatingBar繪製評分星星
@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	int height = getMeasuredHeight();
	int drawableHeight = (int) (height * (1 - 2 * PADDING_PERCENT));
	int drawableWidth = drawableHeight;
	int top = (int) (PADDING_PERCENT * height);
	int left = 0;

	float value = mValue / mMaxValue * 5.0f;
	int full = (int) value;
	int empty = full + 1;
	for (int i = 0; i < full; i++) { // 繪製有分數的星星
		left =  i * (drawableWidth + mStarPadding);
		mRect.set(left, top, left + drawableWidth, top + drawableHeight);
		// 第二個參數爲空代表繪製星星整體
		canvas.drawBitmap(mFullBitmap, null, mRect, mPaint);	
}

    // 繪製有分數部分 
	float fullPart = value - full;
	left = full * (drawableWidth + mStarPadding);
	mRect.set(left, top, (int) (left + drawableWidth * fullPart), top + drawableHeight);
	int realWidth = mFullBitmap.getWidth(), realHeight = mFullBitmap.getHeight();
	// 只繪製星星mRect部分到mLeftRect區域,也就是灰色部分
	mLeftRect.set(0, 0, (int) (realWidth * fullPart), realHeight);
	canvas.drawBitmap(mFullBitmap, mLeftRect, mRect, mPaint);

    // 繪製無分數部分
	float emptyPart = 1.0f - fullPart;
	left = (int) (left + drawableWidth * fullPart);
	mRect.set(left, top, (int) (left + drawableWidth * emptyPart), top + drawableHeight);
	// 只繪製星星mRect部分到mRightRect區域,也就是空白部分
	mRightRect.set((int) (realWidth * fullPart), 0, realWidth, realHeight);
	canvas.drawBitmap(mEmptyBitmap, mRightRect, mRect, mPaint);

	// 繪製沒分數的星星
	for (int i = empty; i < 5; i++) {
		left = i * (drawableWidth + mStarPadding);
		mRect.set(left, top, left + drawableWidth, top + drawableHeight);
		canvas.drawBitmap(mEmptyBitmap, null, mRect, mPaint);
	}
}

onDraw()方法會傳入Canvas畫布對象,畫布對象的drawBitmap()方法可以繪製位圖對象。代碼4-14展示了評分控件內部繪製評分星星的詳細過程,繪製時對於整數類型的評分值可以簡單的繪製整數個fullStar和整數個emptyStar,對於小數類型的評分值就稍微複雜一點,小數的整數部分需要繪製fullStar,比小數大的整數部分繪製emptyStar,小數值所在星星前半部分使用fullStar繪製,後半部分需要用emptyStar繪製。Canvas的繪製位圖方法drawBitmap()有四個參數,第一個參數bitmap代表需要被繪製到畫布上的位圖,第二個參數srcRegion代表需要被繪製的bitmap區域,第三個參數dstRegion代表圖片要被繪製的目標視圖位置,最後的paint參數代表執行繪製時使用的畫筆對象。
在這裏插入圖片描述

上圖上半部分展示了只有整數評分值的情況,下半部分展示的是小數部分所在的星星繪製,星星的左邊部分繪製的是有背景的星星部分,右邊部分繪製的是沒有背景的星星部分,需要注意星星圖片大小realWidth和界面中繪製的星星大小drawableWidth是不完全相同的,圖片的大小跟用戶提供的素材有關係,而星星繪製的大小和視圖測量大小有關係。有背景部分的寬度上佔據整個星星的fullPart(value減去full)大小,它在星星圖片中的寬度也就是(int) (realWidth * fullPart),可以確定要繪製的部分在整個帶背景星星中的srcRegion爲(0, 0, (int) (realWidth * fullPart), realHeight),接着考慮這部分的背景星星需要繪製到View視圖的位置,前面已經展示了full個有背景星星,而且星星之間的距離爲mStarPadding,繪製目標的左邊位置爲full * (drawableWidth + mStarPadding),右邊位置爲每個星星在視圖中的寬度值乘以fullPart的值,最終目標dstRegion即爲(full * (drawableWidth + mStarPadding), top, (int) (left + drawableWidth * fullPart), top + drawableHeight)。右部分無背景星星的繪製與之類似,不再贅述。

當用戶用手指觸摸評分控件時評分值會隨着用戶手指的位置而改變,當用戶手指離開評分控件時評分值應該更新成用戶手指最後接觸地方所代表的的評分值。View.dispatchTouchEvent()方法就能夠獲取用戶手指的觸摸事件,可以把控件的內部分成五個矩形等分,用戶手指在某個矩形內部就判定當前的評分值爲所在矩形索引。

//  RatingBar用戶觸摸交互代碼
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    Log.e(TAG, "x = " + x + ", y = " + y);
    changeValue(x, y);
    return super.dispatchTouchEvent(event);
}

private void changeValue(int x, int y) {
    mTmpRect.set(0, 0, getWidth(), getHeight());
    int height = getMeasuredHeight();
    int drawableHeight = (int) (height * (1 - 2 * PADDING_PERCENT));
    int drawableWidth = drawableHeight;
    int left = 0;
    Log.e(TAG, "total rect = " + mTmpRect.flattenToString());
    if (mTmpRect.contains(x, y)) {
        for (int i = 0; i < 5; i++) { // 判斷用戶觸摸位置在哪個星星展示的位置
            left = (drawableWidth + mStarPadding) * i;
            mTmpRect.set(left, 0, left + drawableWidth, height);
            Log.e(TAG, "current rect = " + mTmpRect.flattenToString());
            if (mTmpRect.contains(x, y)) { // 用戶手指在mTmpRect矩形內
             float point = 1.0f * (x - left) / drawableWidth;
             Log.e(TAG, "point = " + point);
             float value = (i + point) / 5 * mMaxValue;
             Log.e(TAG, "value = " + value);
             setValue(value); // 設置評分控件的值
             break;
            }
        }
    }
}

以上就是自定義的評分控件實現,涉及到自定義屬性,控件大小測量,控件內容繪製和用戶交互等多個接口。
自定義評分控件Demo

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