Android 自定義View之邊緣凹凸的優惠券效果

本篇文章講的是自定義View之邊緣凹凸的優惠券效果,之前有見過很多優惠券的效果都是使用了邊緣凹凸的樣式。和往常一樣,主要總結一下在自定義View的開發過程中需要注意的一些地方。

按照慣例,我們先來看看效果圖
這裏寫圖片描述

一、寫代碼之前,我們先弄清楚view的啓動過程:
之所以想要弄清楚這個問題是因爲代碼裏面用到了onSizeChanged()方法,一開始我有點猶豫onSizeChanged是在什麼時候啓動的呢,所以看看View的啓動流程吧

package per.lijuan.coupondisplayviewdome;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.LinearLayout;

/**
 * 自定義邊緣凹凸的優惠券效果view
 * Created by lijuan on 2016/9/26.
 */
public class CouponDisplayView extends LinearLayout {

    public CouponDisplayView(Context context) {
        this(context, null);
        Log.d("mDebug", "CouponDisplayView:context");
    }

    public CouponDisplayView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        Log.d("mDebug", "CouponDisplayView:context,attrs");
    }

    public CouponDisplayView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.d("mDebug", "CouponDisplayView:context,attrs,defStyleAttr");
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.d("mDebug", "onSizeChanged:w=" + w + ",h=" + h + ",oldw=" + oldw + ",oldh=" + oldh);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d("mDebug", "onDraw");
    }

}

輸出如下:

09-27 15:29:31.957 8210-8210/per.lijuan.coupondisplayviewdome D/mDebug: CouponDisplayView:context,attrs,defStyleAttr
09-27 15:29:31.957 8210-8210/per.lijuan.coupondisplayviewdome D/mDebug: CouponDisplayView:context,attrs
09-27 15:29:32.050 8210-8210/per.lijuan.coupondisplayviewdome D/mDebug: onSizeChanged:w=984,h=361,oldw=0,oldh=0
09-27 15:29:32.083 8210-8210/per.lijuan.coupondisplayviewdome D/mDebug: onDraw

在這裏可以看到,onSizeChanged()方法的啓動是在onDraw之前

二、view的幾個常用觸發方法
1. onFinishInflate():當View中所有的子控件均被映射成xml後觸發
2. onMeasure(int widthMeasureSpec, int heightMeasureSpec):確定所有子元素的大小
3. onLayout(boolean changed, int l, int t, int r, int b):當View分配所有的子元素的大小和位置時觸發
4. onSizeChanged(int w, int h, int oldw, int oldh):當view的大小發生變化時觸發
5. onDraw(Canvas canvas):負責將View繪製在屏幕上

三、View 的幾個構造函數
1、public CouponDisplayView(Context context)
—>Java代碼直接new一個CouponDisplayView實例的時候,會調用這個只有一個參數的構造函數;

2、public CouponDisplayView(Context context, AttributeSet attrs)
—>在默認的XML佈局文件中創建的時候調用這個有兩個參數的構造函數。AttributeSet類型的參數負責把XML佈局文件中所自定義的屬性通過AttributeSet帶入到View內;

3、public CouponDisplayView(Context context, AttributeSet attrs, int defStyleAttr)
—>構造函數中第三個參數是默認的Style,這裏的默認的Style是指它在當前Application或者Activity所用的Theme中的默認Style,且只有在明確調用的時候纔會調用

4、public CouponDisplayView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
—>該構造函數是在API21的時候才添加上的

自定義View中,我們需要重寫了3個構造方法,在上面的構造方法中說過默認的佈局文件調用的是兩個參數的構造方法,所以記得讓所有的構造方法調用三個參數的構造方法,然後在三個參數的構造方法中獲得自定義屬性。
一開始一個參數的構造方法和兩個參數的構造方法是這樣的:

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

    public CouponDisplayView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

我們需要注意的是super應該改成this,然後讓一個參數的構造方法引用兩個參數的構造方法,兩個參數的構造方法引用三個參數的構造方法,代碼如下:

public CouponDisplayView(Context context) {
        this(context, null);
    }

    public CouponDisplayView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

四、分析具體的實現思路:
從上面的效果圖來看,這個自定義View和普通的Linearlayout,RelativeLayout一樣,只是上下兩邊多了類似於半圓鋸齒的形狀,我們需要在上下兩條線上畫一個個白色的小圓來實現這種效果。

假如我們上下線的半圓以及半圓與半圓之間的間距是固定的,那麼不同尺寸的屏幕肯定會畫出不同數量的半圓,那麼我們只需要根據控件的寬度來獲取能畫的半圓數。

我們觀察效果圖會發現,圓的數量總是圓間距數量-1,也就是說,假設圓的數量是circleNum,那麼圓間距就是circleNum+1,所以我們可以根據這個計算出circleNum:

circleNum = (int) ((w-gap)/(2*radius+gap)); 

這裏gap就是圓間距,radius是圓半徑,w是view的寬。

五、下面我們就開始來看看代碼啦
1、自定義View的屬性,首先在res/values/ 下建立一個attr.xml , 在裏面定義我們的需要用到的屬性以及聲明相對應屬性的取值類型

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--圓間距-->
    <attr name="radius" format="dimension" />
    <!--半徑-->
    <attr name="gap" format="dimension" />

    <declare-styleable name="CouponDisplayView">
        <attr name="radius" />
        <attr name="gap" />
    </declare-styleable>

</resources>

我們定義了圓間距和半徑2個屬性,format是值該屬性的取值類型,format取值類型總共有10種,包括:string,color,demension,integer,enum,reference,float,boolean,fraction和flag。

2、然後在XML佈局中聲明我們的自定義View

<?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-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="16dp">

    <per.lijuan.coupondisplayviewdome.CouponDisplayView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#FBB039"
        android:orientation="horizontal"
        android:padding="16dp"
        custom:gap="8dp"
        custom:radius="5dp">

        <ImageView
            android:layout_width="90dp"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@mipmap/ic_launcher" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="電影新客代金劵"
                android:textSize="18dp" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="5dp"
                android:text="編號:525451122312431"
                android:textSize="12dp" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="5dp"
                android:text="滿200元可用、限最新版本客戶端使用"
                android:textSize="12dp" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="5dp"
                android:text="截止日期:2016-11-07"
                android:textSize="12dp" />
        </LinearLayout>
    </per.lijuan.coupondisplayviewdome.CouponDisplayView>
</LinearLayout>

一定要引入xmlns:custom=”http://schemas.android.com/apk/res-auto”,Android Studio中我們可以使用res-atuo命名空間,就不用添加自定義View全類名。

3、在View的構造方法中,獲得我們的xml佈局文件中定義的圓的半徑和圓間距

private Paint mPaint;
    /**
     * 半徑
     */
    private float radius=10;
    /**
     * 圓間距
     */
    private float gap=8;

    /**
     * 圓數量
     */
    private int circleNum;
    private float remain;

    public CouponDisplayView(Context context) {
        this(context, null);
        Log.d("mDebug", "CouponDisplayView context");
    }

    public CouponDisplayView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        Log.d("mDebug", "CouponDisplayView context, attrs");
    }

    public CouponDisplayView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.d("mDebug", "CouponDisplayView context,attrs,defStyleAttr");
        /**
         * 獲得我們所定義的自定義樣式屬性
         */
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CouponDisplayView, defStyleAttr, 0);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.CouponDisplayView_radius:
                    radius = a.getDimensionPixelSize(R.styleable.CouponDisplayView_radius, 10);
                    break;
                case R.styleable.CouponDisplayView_gap:
                    gap = a.getDimensionPixelSize(R.styleable.CouponDisplayView_radius, 8);
                    break;
            }
        }
        a.recycle();

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setDither(true);
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.FILL);
    }

4、重寫onSizeChanged()方法,根據上面的圓的半徑和圓間距來計算需要畫的圓數量circleNum

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.d("mDebug", "onSizeChanged,w=" + w + ",h=" + h + ",oldw=" + oldw + ",oldh=" + oldh);
        if (remain == 0) {
            //計算不整除的剩餘部分
            remain = (int) (w - gap) % (2 * radius + gap);
        }
        circleNum = (int) ((w - gap) / (2 * radius + gap));
    }

5、接下來只需要重寫onDraw()方法,簡單的根據circleNum的數量將一個一個的圓繪製在屏幕上就可以了

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d("mDebug", "onDraw");
        for (int i = 0; i < circleNum; i++) {
            float x = gap + radius + remain / 2 + ((gap + radius * 2) * i);
            canvas.drawCircle(x, 0, radius, mPaint);
            canvas.drawCircle(x, getHeight(), radius, mPaint);
        }
    }

這裏remain/2是因爲避免有一些情況:當計算出來的圓的數量不是整除時,這樣就會出現右邊最後一個間距會比其它的間距都要寬,所以我們在繪製第一個的時候加上了餘下的間距的一半,即使是不整除的情況,至少也能保證第一個和最後一個間距寬度一致。

好了,本篇文章已經全部寫完了,存在總結不到位的地方還望指導,感謝^_^

源碼下載

參考資料:
http://blog.csdn.net/yissan/article/details/51429281

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