前言
在写这个之前也是因为在项目中使用到了类似 优惠卷 的这个东西,然后呢我就在网上找了一些demo看,但是发现他们都存在一些缺陷而且比较单一功能太简单。为什么这么说呢,我给大家说明一下,请看下图:
canvas
的
drawCircle
方法被绘制出来的,并且绘制在图层之上,所以无法显示出下面的背景图。而且只提供了半圆这一种图形,所以我说相对比较单一。正因如此我来给大家介绍我的
CouponView 最好的Android优惠卷 。先放几张效果图,再讲解实现过程
1.特点
- 提供了4种边缘图案(圆形,椭圆,三角,正方)
- 可自定义灵活的api支持
- 以剪裁的方案切入,可展示覆盖在下面的背景图
- 使用模板方法设计模式完成本例
2.效果图
已在 Github 开源:CouponView,欢迎 Star !
3.实现过程讲解
3.1 类文件结构
DrawModel: CouponModel的父类定义一个绘制的框架控制行为,和初始化Paint
等工作。
CouponModel: 实现父类的抽象方法,进行具体的绘制
CouponView: view
继承 FrameLayout 负责接收自定义的属性,和处理相关操作,通过调用 DrawModel
的模板方法 drawing()
实现边缘图形的绘制
3.2源码分析
- 先来看CouponView
步骤1: 初始化自定义属性
public CouponView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CouponView, 0, 0);
//边缘图形类型
drawType = typedArray.getInt(R.styleable.CouponView_drawShapeType, CIRCLE);
//图形与图形之间的间隙
dashGap = typedArray.getDimensionPixelOffset(R.styleable.CouponView_dashGap, 5);
//半径
dashWidth = typedArray.getDimensionPixelOffset(R.styleable.CouponView_dashWidth, 10);
//背景色
bgc = typedArray.getColor(R.styleable.CouponView_bgc, bgc);
//虚线的颜色
lineColor = typedArray.getColor(R.styleable.CouponView_lineColor, Color.WHITE);
//虚线的宽度
lineWidth = typedArray.getDimensionPixelOffset(R.styleable.CouponView_lineWidth, dpToPx(2));
//是否绘制左边边缘的虚线
isDrawLeftLine = typedArray.getBoolean(R.styleable.CouponView_isDrawLeftLine, false);
isDrawTopLine = typedArray.getBoolean(R.styleable.CouponView_isDrawTopLine, false);
isDrawRightLine = typedArray.getBoolean(R.styleable.CouponView_isDrawRightLine, false);
isDrawBottomLine = typedArray.getBoolean(R.styleable.CouponView_isDrawBottomLine, false);
//是否绘制左边边缘的图形
isDrawLeftShape = typedArray.getBoolean(R.styleable.CouponView_isDrawLeftShape, false);
isDrawTopShape = typedArray.getBoolean(R.styleable.CouponView_isDrawTopShape, false);
isDrawRightShape = typedArray.getBoolean(R.styleable.CouponView_isDrawRightShape, false);
isDrawBottomShape = typedArray.getBoolean(R.styleable.CouponView_isDrawBottomShape, false);
//虚线距离自身顶部距离
lineMarginTop = typedArray.getDimensionPixelOffset(R.styleable.CouponView_lineMarginTop, dashWidth);
lineMarginBottom = typedArray.getDimensionPixelOffset(R.styleable.CouponView_lineMarginBottom, dashWidth);
lineMarginLeft = typedArray.getDimensionPixelOffset(R.styleable.CouponView_lineMarginLeft, dashWidth);
lineMarginRight = typedArray.getDimensionPixelOffset(R.styleable.CouponView_lineMarginRight, dashWidth);
typedArray.recycle();
}
}
复制代码
步骤2:初始化绘制图层&计算边缘图形的数量
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initDrawCanvas(w, h);
calculateItemNum(w, h);
}
/**
* 初始化绘制图层
*
* @param w
* @param h
*/
private void initDrawCanvas(int w, int h) {
if (getBackground() == null) {//背景为空,设置为透明
setBackgroundColor(Color.TRANSPARENT);
}
// 初始化遮盖图层
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
//1.将Canvas指定区域转换为一个Bitmap对象,或者可以理解为将Bitmap保存到Canvas中
//2.在onDraw方法里面Canvas将新建这个Bitmap对象,新建一个画布
mCanvas = new Canvas(mBitmap);
// 绘制图层颜色
mCanvas.drawColor(bgc);
//初始化DrawModel
drawModel = new CouponModel(this);
drawModel.setCanvas(mCanvas);
}
/**
* item数量的 计算公式 :
* Num = ( w - dashGap ) / ( 2 * dashWidth + dashGap);
*/
public void calculateItemNum(int w, int h) {
if (isDrawLeftLine || isDrawRightLine || isDrawLeftShape || isDrawRightShape) {
drawModel.measureVelNum(h);
}
if (isDrawTopLine || isDrawBottomLine || isDrawTopShape || isDrawBottomShape) {
drawModel.measureHorNum(w);
}
}
复制代码
步骤3:绘制锯齿
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//新建一个bitmap进行绘制
canvas.drawBitmap(mBitmap, 0, 0, null);
//DrawModel处理绘制
drawModel.drawing();
}
复制代码
DrawModel
public abstract class DrawModel {
protected final CouponView view;
//item数量
protected int horItemSize;
protected int verItemSize;
//水平方向除item外多余的部分
protected float horRedundancy;
//垂直方向除item外多余的部分
protected float verRedundancy;
protected Paint shapePaint;
protected Paint linePaint;
protected Canvas mCanvas;
private void initPaint() {
//初始化绘制边缘图形的paint
shapePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
shapePaint.setDither(true);
shapePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
shapePaint.setStyle(Paint.Style.FILL);
//初始化绘制虚线的paint
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint.setStyle(Paint.Style.FILL);
linePaint.setColor(view.getLineColor());
linePaint.setStrokeWidth(view.getLineWidth());
}
public DrawModel(CouponView view) {
this.view = view;
initPaint();
}
public void setCanvas(Canvas canvas) {
this.mCanvas = canvas;
}
/**
* 绘制的入口
*/
public void drawing() {//模板方法
if (view.getDrawType() == CIRCLE) {
drawCircle();
} else if (view.getDrawType() == OVAL) {
drawOval();
} else if (view.getDrawType() == TRIANGLE) {
drawTriangle();
} else if (view.getDrawType() == SQUARE) {
drawSquare();
}
if (view.isDrawLeftLine() || view.isDrawRightLine() || view.isDrawTopLine() || view.isDrawTopLine()) {
drawLine();
}
}
/**
* 测量垂直item数目
*
* @param h
*/
public void measureVelNum(int h) {
if (verRedundancy == 0) {
verRedundancy = (h - view.getDashGap()) % (view.getDashWidth() * 2 + view.getDashGap());
}
verItemSize = (h - view.getDashGap()) / (view.getDashWidth() * 2 + view.getDashGap());
}
/**
* 测量水平的item数目
*
* @param w
*/
public void measureHorNum(int w) {
if (horRedundancy == 0) {
horRedundancy = (w - view.getDashGap()) % (view.getDashWidth() * 2 + view.getDashGap());
}
horItemSize = (w - view.getDashGap()) / (view.getDashWidth() * 2 + view.getDashGap());
}
/**
* 绘制正方形
*/
protected abstract void drawSquare();
/**
* 绘制三角形
*/
protected abstract void drawTriangle();
/**
* 绘制椭圆
*/
protected abstract void drawOval();
/**
* 绘制三角形
*/
protected abstract void drawCircle();
/**
* 绘制虚线
*/
protected abstract void drawLine();
}
复制代码
作为模板方法设计模式的父类提供了一个模板方法 drawing()
实现对基本方法的调度这也是这个设计模式的定义表现,以及初始化 Paint
和计算出水平方向垂直方向边缘图形的数量。关于设计模式可以参考我的这些文章那些你要知道的设计模式
public class CouponModel extends DrawModel {
public CouponModel(CouponView view) {
super(view);
}
@Override
protected void drawCircle() {
//遍历垂直方向可绘制size
for (int i = 0; i < verItemSize; i++) {
//圆心位置 = 间隙 + item半径 + 垂直方向多余部分/2 + (间隙 + item半径*2) * i
float y = view.getDashGap() + view.getDashWidth() + verRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
if (view.isDrawLeftShape()) {
mCanvas.drawCircle(0, y, view.getDashWidth(), shapePaint);
}
if (view.isDrawRightShape()) {
mCanvas.drawCircle(view.getWidth(), y, view.getDashWidth(), shapePaint);
}
}
//遍历水平方向可绘制size
for (int i = 0; i < horItemSize; i++) {
float x = view.getDashGap() + view.getDashWidth() + horRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
if (view.isDrawTopShape()) {
mCanvas.drawCircle(x, 0, view.getDashWidth(), shapePaint);
}
if (view.isDrawBottomShape()) {
mCanvas.drawCircle(x, view.getHeight(), view.getDashWidth(), shapePaint);
}
}
}
@Override
protected void drawOval() {
for (int i = 0; i < horItemSize; i++) {
float x = view.getDashGap() + view.getDashWidth() + horRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
RectF rectf = new RectF();
// 设置椭圆大小
rectf.left = x - view.getDashWidth();
rectf.right = x + view.getDashWidth();
// 绘制上面的椭圆
if (view.isDrawTopShape()) {
rectf.top = 0;
rectf.bottom = view.getDashWidth();
mCanvas.drawOval(rectf, shapePaint);
}
// 绘制下面的椭圆
if (view.isDrawBottomShape()) {
rectf.top = view.getHeight() - view.getDashWidth();
rectf.bottom = view.getHeight();
mCanvas.drawOval(rectf, shapePaint);
}
}
for (int i = 0; i < verItemSize; i++) {
float y = view.getDashGap() + view.getDashWidth() + verRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
// 定义椭圆对象
RectF rectf = new RectF();
// 设置椭圆大小
rectf.top = y - view.getDashWidth();
rectf.bottom = y + view.getDashWidth();
// 绘制椭圆
if (view.isDrawLeftShape()) {
rectf.left = 0;
rectf.right = view.getDashWidth();
mCanvas.drawOval(rectf, shapePaint);
}
// 绘制椭圆
if (view.isDrawRightShape()) {
rectf.left = view.getWidth() - view.getDashWidth();
rectf.right = view.getWidth();
mCanvas.drawOval(rectf, shapePaint);
}
}
}
@Override
protected void drawSquare() {
for (int i = 0; i < verItemSize; i++) {
float y = view.getDashGap() + view.getDashWidth() + verRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
RectF rectf = new RectF();
// 设置正方形大小
rectf.top = y - view.getDashWidth() / 2;
rectf.bottom = y + view.getDashWidth();
// 如果isDrawLeftSide为true
if (view.isDrawLeftShape()) {
rectf.left = 0;
rectf.right = view.getDashWidth() ;
mCanvas.drawRect(rectf, shapePaint);
}
if (view.isDrawRightShape()) {
rectf.left = view.getWidth() - view.getDashWidth();
rectf.right = view.getWidth();
mCanvas.drawRect(rectf, shapePaint);
}
}
for (int i = 0; i < horItemSize; i++) {
float x = view.getDashGap() + view.getDashWidth() + horRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
mCanvas.drawRect(0, x, 0, view.getDashWidth(), shapePaint);
RectF rectf = new RectF();
// 设置正方形大小
rectf.left = x - view.getDashWidth() / 2;
rectf.right = x + view.getDashWidth() ;
// 如果isDrawTopSide为true
if (view.isDrawTopShape()) {
rectf.top = 0;
rectf.bottom = view.getDashWidth();
mCanvas.drawRect(rectf, shapePaint);
}
if (view.isDrawBottomShape()) {
rectf.top = view.getHeight() - view.getDashWidth();
rectf.bottom = view.getHeight();
mCanvas.drawRect(rectf, shapePaint);
}
}
}
@Override
protected void drawTriangle() {
for (int i = 0; i < verItemSize; i++) {
float y = view.getDashGap() + view.getDashWidth() + verRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
Path path = new Path();
//如果isDrawLeftSide为true
if (view.isDrawLeftShape()) {
// 设置三角形的点
path.moveTo(0, y - view.getDashWidth());
path.lineTo(0, y + view.getDashWidth());
path.lineTo(view.getDashWidth(), y);
path.close();
mCanvas.drawPath(path, shapePaint);
}
if (view.isDrawRightShape()) {
path.moveTo(view.getWidth(), y - view.getDashWidth());
path.lineTo(view.getWidth(), y + view.getDashWidth());
path.lineTo(view.getWidth() - view.getDashWidth(), y);
path.close();
mCanvas.drawPath(path, shapePaint);
}
}
for (int i = 0; i < horItemSize; i++) {
float x = view.getDashGap() + view.getDashWidth() + horRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
Path path = new Path();
//如果isDrawTopSide为true
if (view.isDrawTopShape()) {
// 设置三角形的点
path.moveTo(x - view.getDashWidth(), 0);
path.lineTo(x + view.getDashWidth(), 0);
path.lineTo(x, view.getDashWidth());
path.close();
mCanvas.drawPath(path, shapePaint);
}
if (view.isDrawBottomShape()) {
path.moveTo(x - view.getDashWidth(), view.getHeight());
path.lineTo(x + view.getDashWidth(), view.getHeight());
path.lineTo(x, view.getHeight() - view.getDashWidth());
path.close();
mCanvas.drawPath(path, shapePaint);
}
}
}
@Override
protected void drawLine() {
//遍历垂直方向可绘制size
for (int i = 0; i < verItemSize; i++) {
//圆心位置 = 间隙 + item半径 + 垂直方向多余部分/2 + (间隙 + item半径*2) * i
float y = view.getDashGap() + view.getDashWidth() + verRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
if (view.isDrawLeftLine()) {
mCanvas.drawLine(view.getLineMarginLeft(), y - view.getDashWidth(), view.getLineMarginLeft(), y + view.getDashWidth(), linePaint);
}
if (view.isDrawRightLine()) {
mCanvas.drawLine(view.getWidth() - view.getLineMarginRight(), y - view.getDashWidth(), view.getWidth() - view.getLineMarginRight(), y + view.getDashWidth(), linePaint);
}
}
//遍历水平方向可绘制size
for (int i = 0; i < horItemSize; i++) {
float x = view.getDashGap() + view.getDashWidth() + horRedundancy / 2 + ((view.getDashGap() + view.getDashWidth() * 2) * i);
if (view.isDrawTopLine()) {
mCanvas.drawLine(x - view.getDashWidth(), view.getLineMarginTop(), x + view.getDashWidth(), view.getLineMarginTop(), linePaint);
}
if (view.isDrawBottomLine()) {
mCanvas.drawLine(x - view.getDashWidth(), view.getHeight() - view.getLineMarginBottom(), x + view.getDashWidth(), view.getHeight() - view.getLineMarginBottom(), linePaint);
}
}
}
}
复制代码
作为子类实现父类中定义的基本方法,绘制圆形,椭圆,三角,正方还有虚线。通过遍历边缘图形的数量size大小,结合自定义属性中的 dashGap
间隙, dashWidthcode
半径,和在 CouponView
的 onSizeChanged
方法中计算得出冗余部分的宽度,确定每个边缘图形的坐标位置进行绘制。
至此,该优惠卷View控件源码分析完毕。感谢阅读!
完整源码地址:https://github.com/xwc520/CouponView
已在 Github 开源:CouponView,欢迎 留言, Star !