Android实用的优惠券控件

前言

最近需要做一个优惠券功能,于是找了找,发现网上大多数优惠券控件的都是直接利用Paint绘制一个白色的新图层然后叠加上去,但是这样处理的话,当背景不是纯白色的时候,就会暴露出如下图问题:

网上常见项目的效果图

对于有点强迫症的人来说,看着怎么都有点难受。好吧,俗话说自己动手丰衣足食,咱就自己动手弄个更加完善的出来。

完整代码项目地址在文章尾部有链接,需要的可以自行下载。

正文

为了解决掉边缘锯齿的问题,我用另外的思路实现了这个功能,虽然已经有较多人造出了轮子,但咱们还是谈一下本项目的优势:

  1. 优惠券控件边缘锯齿形状有:圆形、椭圆、三角形、正方形四种样式可选;
  2. 锯齿的间距、大小、控件颜色等可自定义设置,定制性和灵活性更强;
  3. 边缘锯齿是抠掉了变成透明的,而不是绘制白色的叠加上去,更符合客观世界实际物质认知。

效果图

这里写图片描述

实现步骤

一开始我想着,哎~?直接把Paint 给设置成DST_OUT 不就完事了吗? 结果试了这种方法之后发现,扣掉的部分不是变成透明,而是黑色... 尴尬,想了想大概是因为LinearLayout有默认处理背景颜色,所以不能直接对控件的Canvas 画布抠图,这样行不通。那么咱另外添加一个Canvas 当做背景然后抠图,自定义控件背景默认透明就好了。

本项目实现的大致思路和步骤如下:

  1. 定义一个VoucherView类继承自LinearLayout;
  2. 在attrs.xml文件里面声明所需自定义属性;
  3. 将自定义控件背景利用代码给设置成完全透明(详见VoucherView类的initDrawCanvas方法);
  4. 创建一个mCanvas画布并通过自定义属性BgColor获取需设定的颜色;
  5. 创建一个mPaint画笔,用于绘制边缘锯齿;
  6. 利用图像合成类PorterDuffXfermode并给Paint画笔设置DST_OUT模式;
  7. 获取自定义属性drawType,根据自定义属性设定的形状,绘制边缘锯齿;
  8. mCanvas画布擦掉mPaint画笔所绘制的形状部分。

源代码

自定义控件 VoucherView类:

package com.voucher;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.widget.LinearLayout;


/**
 * @TODO<自定义优惠券控件>
 * @author 小嵩
 * @date 2017-4-14
 */

public class VoucherView extends LinearLayout {
    private Paint mPaint;

    //item间距 默认是5dp
    private float mGap;

    //绘制的图层
    private Bitmap mBitmap;
    private Canvas mCanvas;

    //item半径 默认是10dp
    private float mRadius ;

    //item数量
    private int mCircleNum_H;
    private int mCircleNum_V;

    //除过item和间隙外多余出来的部分
    private float mRemain_H;//水平
    private float mRemain_V;//垂直


    //画笔颜色
    private int mPaintColor;

    //指定绘制的方向
    private int mOrientation;
    public final static int DRAW_HORIZONTAL = 0;//水平
    public final static int DRAW_VERTICAL = 1;//垂直
    public final static int DRAW_AROUND = 2;//全部

    //锯齿形状 (圆形,椭圆,三角形,正方形)
    private int drawType;
    private static final int CIRCLE = 0;
    private static final int ELLIPSE = 1;
    private static final int TRIANGLE = 2;
    private static final int SQUARE = 3;



    @Override
    public void setOrientation(int orientation) {
    }

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

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

    }

    public VoucherView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

          if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.voucherView, 0, 0);
            drawType = a.getInt(R.styleable.voucherView_drawType, CIRCLE);
            mOrientation = a.getInt(R.styleable.voucherView_orientation,DRAW_HORIZONTAL);//默认水平方向
            mGap = a.getDimensionPixelOffset(R.styleable.voucherView_mGap, 5);
            mRadius = a.getDimensionPixelOffset(R.styleable.voucherView_mRadius, 10);
            mPaintColor = a.getColor(R.styleable.voucherView_BgColor, 0xFFc0c0c0);
            a.recycle();//回收内存
        }
        
        initPaint();
    }

    private void initPaint() {//边缘锯齿画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setDither(true);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        mPaint.setStyle(Paint.Style.FILL);
    }

    /**
     *  item数量的 计算公式 :
     *  circleNum = (int) ((w-gap)/(2*radius+gap));
     */

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        initDrawCanvas(w, h);

        switch (mOrientation){
            case DRAW_HORIZONTAL:
                measureHorNum(w);
                break;
            case DRAW_VERTICAL:
                measureVelNum(h);
                break;
            case DRAW_AROUND:
                measureHorNum(w);
                measureVelNum(h);
                break;
        }
    }


    /**
     * 初始化绘制图层
     * @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);
        mCanvas = new Canvas(mBitmap);
        // 绘制图层颜色
        mCanvas.drawColor(mPaintColor);
    }

    /**
     * 测量水平的item数目
     * @param w
     */
    private void measureHorNum(int w) {
        if(mRemain_H==0){
            mRemain_H=(w-mGap)%(mRadius*2+mGap);
        }
        mCircleNum_H=(int)((w-mGap)/(mRadius*2+mGap));
    }
    /**
     * 测量垂直item数目
     * @param h
     */
    private void measureVelNum(int h) {
        if(mRemain_V==0){
            mRemain_V=(h-mGap)%(mRadius*2+mGap);
        }
        mCircleNum_V=(int)((h-mGap)/(mRadius*2+mGap));
    }


    /**
     * 绘制锯齿
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(mBitmap, 0, 0, null);//绘制图层

       switch (mOrientation){

           /**
            * 水平方向
            */
           case DRAW_HORIZONTAL:
               if (drawType==CIRCLE){//圆形
                   drawHorCircle();
               }else if (drawType==ELLIPSE){//椭圆
                   drawHorEllipse();
               }else if(drawType==TRIANGLE){//三角形
                   drawHorTriangle();
               }else if(drawType==SQUARE){//正方形
                   drawHorSquare();
               }
               break;

           /**
            * 垂直方向
            */
           case DRAW_VERTICAL:
               if (drawType==CIRCLE){//圆形
                   drawVelCircle();
               }else if (drawType==ELLIPSE){//椭圆
                   drawVelEllipse();
               }else if(drawType==TRIANGLE){//三角形
                   drawVelTriangle();
               }else if(drawType==SQUARE){//正方形
                   drawVelSquare();
               }
               break;

           /**
            * 四周方向
            */
           case DRAW_AROUND:
               if (drawType==CIRCLE){//圆形
                   drawHorCircle();
                   drawVelCircle();
               }else if (drawType==ELLIPSE){//椭圆
                   drawHorEllipse();
                   drawVelEllipse();
               }else if (drawType==TRIANGLE){//三角形
                   drawHorTriangle();
                   drawVelTriangle();
               }else if(drawType==SQUARE){//正方形
                   drawHorSquare();
                   drawVelSquare();
               }
               break;
       }
    }


    ////***********************************************************////

    /**
     * 绘制水平的圆
     */
    private void drawHorCircle() {
        for (int i=0;i<mCircleNum_H;i++){
            float x = mGap+mRadius+mRemain_H/2+((mGap+mRadius*2)*i);
            mCanvas.drawCircle(x,0,mRadius,mPaint);
            mCanvas.drawCircle(x,getHeight(),mRadius,mPaint);
        }
    }

    /**
     * 绘制水平的椭圆
     */
    private void drawHorEllipse() {
        for (int i=0;i<mCircleNum_H;i++){
            float x = mGap+mRadius+mRemain_H/2+((mGap+mRadius*2)*i);
            // 定义椭圆对象
            RectF rectf = new RectF();
            // 设置椭圆大小
            rectf.left = x-mRadius;
            rectf.right = x+mRadius;
            rectf.top = 0;
            rectf.bottom = mRadius;
            // 绘制上面的椭圆
            mCanvas.drawOval(rectf, mPaint);
            rectf.top = getHeight()-mRadius;
            rectf.bottom = getHeight();
            // 绘制下面的椭圆
            mCanvas.drawOval(rectf, mPaint);
        }
    }

    /**
     * 绘制水平的三角形
     */
    private void drawHorTriangle() {
        for (int i=0;i<mCircleNum_H;i++){
            float x = mGap+mRadius+mRemain_H/2+((mGap+mRadius*2)*i);
            // 绘制三角形
            Path path = new Path();
            // 设置多边形的点
            path.moveTo(x-mRadius,0);
            path.lineTo(x+mRadius,0);
            path.lineTo(x, mRadius);
            path.lineTo(x-mRadius,0);
            // 使这些点构成封闭的多边形
            path.close();
            mCanvas.drawPath(path,mPaint);

            //绘制下边缘
            path.moveTo(x-mRadius,getHeight());
            path.lineTo(x+mRadius,getHeight());
            path.lineTo(x,getHeight()-mRadius);
            path.lineTo(x-mRadius,getHeight());
            // 使这些点构成封闭的多边形
            path.close();
            mCanvas.drawPath(path,mPaint);
        }
    }
    /**
     * 绘制水平的正方形
     */
    private void drawHorSquare() {
        for (int i=0;i<mCircleNum_H;i++){
            float x = mGap+mRadius+mRemain_H/2+((mGap+mRadius*2)*i);

            mCanvas.drawRect(0,x,0,mRadius,mPaint);
            // 定义正方形对象
            RectF rectf = new RectF();
            // 设置正方形大小
            rectf.left = x-mRadius/2;
            rectf.right = x+mRadius/2;
            rectf.top = 0;
            rectf.bottom = mRadius;
            // 绘制上面的正方形
            mCanvas.drawRect(rectf, mPaint);
            rectf.top = getHeight()-mRadius;
            rectf.bottom = getHeight();
            // 绘制下面的正方形
            mCanvas.drawRect(rectf, mPaint);
        }
    }

    ////***********************************************************////

    /**
     * 绘制垂直的圆
     */
    private void drawVelCircle() {
        for (int i=0;i<mCircleNum_V;i++){
            float y = mGap+mRadius+mRemain_V/2+((mGap+mRadius*2)*i);
            mCanvas.drawCircle(0,y,mRadius,mPaint);
            mCanvas.drawCircle(getWidth(),y,mRadius,mPaint);
        }
    }


    /**
     * 绘制垂直的椭圆
     */
    private void drawVelEllipse() {
        for (int i=0;i<mCircleNum_V;i++){
            float y = mGap+mRadius+mRemain_V/2+((mGap+mRadius*2)*i);
            // 定义椭圆对象
            RectF rectf = new RectF();
            // 设置椭圆大小
            rectf.left = 0;
            rectf.right = mRadius;
            rectf.top = y-mRadius;
            rectf.bottom = y+mRadius;
            // 绘制椭圆
            mCanvas.drawOval(rectf, mPaint);
            rectf.left = getWidth()-mRadius;
            rectf.right = getWidth();
            // 绘制椭圆
            mCanvas.drawOval(rectf, mPaint);
        }
    }

    /**
     * 绘制垂直的三角形
     */
    private void drawVelTriangle() {
        for (int i=0;i<mCircleNum_V;i++){
            float y = mGap+mRadius+mRemain_V/2+((mGap+mRadius*2)*i);
            // 绘制三角形
            Path path = new Path();
            // 设置多边形的点
            path.moveTo(0,y-mRadius);
            path.lineTo(0,y+mRadius);
            path.lineTo(mRadius,y);
            path.lineTo(0,y-mRadius);
            // 使这些点构成封闭的多边形
            path.close();
            mCanvas.drawPath(path,mPaint);

            //绘制下边缘
            path.moveTo(getWidth(),y-mRadius);
            path.lineTo(getWidth(),y+mRadius);
            path.lineTo(getWidth()-mRadius,y);
            path.lineTo(getWidth(),y-mRadius);
            // 使这些点构成封闭的多边形
            path.close();
            mCanvas.drawPath(path,mPaint);
        }
    }


    /**
     * 绘制垂直的椭圆
     */
    private void drawVelSquare() {
        for (int i=0;i<mCircleNum_V;i++){
            float y = mGap+mRadius+mRemain_V/2+((mGap+mRadius*2)*i);
            // 定义椭圆对象
            RectF rectf = new RectF();
            // 设置椭圆大小
            rectf.left = 0;
            rectf.right = mRadius/2;
            rectf.top = y-mRadius/2;
            rectf.bottom = y+mRadius;
            // 绘制椭圆
            mCanvas.drawRect(rectf, mPaint);
            rectf.left = getWidth()-mRadius;
            rectf.right = getWidth();
            // 绘制椭圆
            mCanvas.drawRect(rectf, mPaint);
        }
    }
}

在values目录下 - 创建 attrs.xml 文件添加如下代码:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="voucherView">

        <attr name="drawType">
            <enum name="circle" value="0"/>
            <enum name="ellipse" value="1"/>
            <enum name="triangle" value="2"/>
            <enum name="square" value="3"/>
        </attr>

        <attr name="orientation">
            <enum name="horizontal" value="0"/>
            <enum name="vertical" value="1"/>
            <enum name="around" value="2"/>
        </attr>

        <attr name="mGap" format="dimension"/>
        <attr name="mRadius" format="dimension"/>
        <attr name="BgColor" format="color"/>

    </declare-styleable>
</resources>

关于使用

</br>
</br>

1.引入依赖

有三种方式:

  • Jcenter库
 compile 'com.xiaosong520:voucher:1.0.1'
  • 引入Module
     下载源代码,然后将voucher组件拷贝到工程去,并添加Module依赖
  • 直接拷贝代码到项目
     直接将项目中 VoucherView 类 以及 attrs.xml 文件里面的自定义属性拷贝到项目中去(Jcenter库后续会添加,具体情况以GitHub项目Readme文档为准)。
    </br>
    </br>

2.在需要使用的布局文件中添加控件,代码如下

(路径请替换成实际项目中 VoucherView 类所在的路径)。

根部局添加如下属性:

xmlns:VoucherView="http://schemas.android.com/apk/res-auto"

添加控件:

 <com.voucher.VoucherView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="30dp"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        VoucherView:drawType="circle"
        VoucherView:orientation="horizontal"
        VoucherView:mGap="5dp"
        VoucherView:mRadius="5dp"
        VoucherView:BgColor="#FFA90F">
        
 <!--优惠券实际内容部分-->
 <include
            layout="@layout/include_content"/>
        
  </com.voucher.VoucherView>

将include 布局部分替换成你实际所需要的布局就行了,接下来万事大吉,可以愉快地使用了~

</br>
</br>

3.可自定义的属性及参数表格(attrs)

method(方法名称) format(参数格式) description(描述)
drawType enum(枚举) 有圆形、椭圆、三角形、正方形这四种边缘锯齿形状
orientation enum(枚举) 包含 horizontal、vertical、around 这三种方向,分别表示水平、垂直、四周。
mGap dimension(尺寸) 该参数控制边缘锯齿之间的间隔宽度
mRadius dimension(尺寸) 该参数控制边缘锯齿的半径长度
BgColor color(颜色) 该参数控制自定义控件的背景颜色

</br>
</br>

GitHub项目链接地址:EasyVoucherView

欢迎提出建议和指出不足,如果感觉对你有帮助的话欢迎Star支持一下,也非常乐意Fork和Pull Request~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容