本篇文章讲的是自定义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
*/
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的几个常用触发方法
- onFinishInflate():当View中所有的子控件均被映射成xml后触发
- onMeasure(int widthMeasureSpec, int heightMeasureSpec):确定所有子元素的大小
- onLayout(boolean changed, int l, int t, int r, int b):当View分配所有的子元素的大小和位置时触发
- onSizeChanged(int w, int h, int oldw, int oldh):当view的大小发生变化时触发
- 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">
<com.tran.mydemo.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>
</com.tran.mydemo.CouponDisplayView
>
</LinearLayout>
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是因为避免有一些情况:当计算出来的圆的数量不是整除时,这样就会出现右边最后一个间距会比其它的间距都要宽,所以我们在绘制第一个的时候加上了余下的间距的一半,即使是不整除的情况,至少也能保证第一个和最后一个间距宽度一致。
好了,本篇文章已经全部写完了!