【Android - 自定义View】之自定义View实现“刮刮卡”效果
首先来介绍一下这个自定义View:
- (1)这个自定义View的名字叫做 GuaguakaView ,继承自View类;
- (2)这个View实现了很多电商项目中的“刮刮卡”的效果,即用户可以刮开覆盖层,查看自己是否中奖;
- (3)用户可以设置覆盖层的图片以及显示的文本内容和字体大小等参数;
- (4)用户可以设置一个阈值,当刮开的面积大于这个阈值时,就会自动清除所有覆盖物。
接下来简单介绍一下在这个自定义View中用到的技术点:
- (1)自定义属性:在 /res/values/attr.xml 文件中定义自定义属性;在XML中使用自定义属性;在自定义View中通过TypedArray获取自定义属性的值;
- (2)在 onMeasure() 方法中处理View的宽高:根据有无前景图片、前景图片宽高、原始分配的宽高来处理这个View显示的宽高,保证:如果有前景图片,则让前景图片以最大比例铺满宽高且不出现失真情况;如果没有设置前景图片,则根据宽高是否是固定值来处理:如果是固定值则铺满整个宽高,如果不是固定值则包裹内容文本;
- (3)由于onMeasure()方法在程序运行时可能会调用多次,因此我们将一些与宽高有关的无关代码放到只会执行一次的 onLayout() 方法中执行,尽量减少重复运行的代码;
- (4)使用 Canvas 、 Paint 、 Path 、 Bitmap 等API,对View进行绘制;
- (5)在 onTouchEvent() 方法中处理Path中的线条,绘制线条;当手指抬起时,判断当前绘制的线条的覆盖度是否达到阈值,如果达到则清除所有覆盖物;
- (6)通过Paint对象的 setXfermode() 方法,设置Paint的绘制模式,达到“刮刮卡”的效果;
- (7)在非onDraw()方法中,调用 invalidate() 方法对View进行重绘,更新View中的绘图;
- (8)设置了一个回调接口 OnGuaguakaUncoverListener ,监听所有覆盖物都被清除的状态,并将事件回调到 onGuaguakaUncovered() 方法中。
下面是这个自定义View—— GuaguakaView 的实现代码:
自定义View类 GuaguakaView.java 中的代码:
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View; /**
* 自定义“刮刮卡”View
*/
public class GuaguakaView extends View {
private int width, height; // 刮刮卡布局最终显示的宽度和高度 private int foreImageRes = -1; // 自定义属性:前景图片
private StringBuffer text = new StringBuffer(); // 自定义属性:显示的文本
private int textSize = -1; // 自定义属性:文本字体大小
private int textColor = Color.BLACK; // 自定义属性:文本颜色
private float uncoverFraction = 0.6f; // 自定义属性:当刮开多少比重的时候消除所有覆盖物
private int strokeWidth = -1; // 自定义属性:刮卡时的线条粗细 private Canvas foreCanvas; // 前景画布,用于绘制前景色、前景图片和刮卡线条
private Paint forePaint; // 用于绘制前景色、前景图片和刮卡线条的画笔
private Paint textPaint; // 用于绘制文本的画笔
private Bitmap foreBm; // 前景画布中的Bitmap对象
private Bitmap foreImg; // 前景图片的Bitmap对象
private Path path; // 刮卡线条
private int[] bmPixels; // 保存前景中所有像素的数组 private boolean isMaskCleared; // 记录前景是否都被消除了
private float textWidth; // 文本的宽度 private OnGuaguakaUncoverListener listener; // 回调接口 public GuaguakaView(Context context) {
this(context, null);
} public GuaguakaView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
} public GuaguakaView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 加载自定义属性
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.GuaguakaView, defStyleAttr, 0);
int attrCount = array.getIndexCount();
for (int i = 0; i < attrCount; i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.GuaguakaView_foreImage:
foreImageRes = array.getResourceId(attr, -1);
break;
case R.styleable.GuaguakaView_text:
text.delete(0, text.length());
text.append(array.getString(attr));
break;
case R.styleable.GuaguakaView_textSize:
textSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, context.getResources().getDisplayMetrics())),
context.getResources().getDisplayMetrics());
break;
case R.styleable.GuaguakaView_textColor:
textColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.GuaguakaView_uncoverFraction:
uncoverFraction = array.getFloat(attr, 0.6f);
break;
case R.styleable.GuaguakaView_strokeWidth:
strokeWidth = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics())),
context.getResources().getDisplayMetrics());
break;
}
}
array.recycle();
// 设置一些初始值
if (textSize == -1) {
textSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, context.getResources().getDisplayMetrics());
}
if (strokeWidth == -1) {
strokeWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());
}
if (foreImageRes != -1) {
foreImg = BitmapFactory.decodeResource(getResources(), foreImageRes);
}
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
textPaint = new Paint();
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
textWidth = textPaint.measureText(text.toString());
// 如果设置了前景图片,则按照图片的宽高比例铺满父布局提供的宽高
if (foreImageRes != -1) {
int imgWidth = foreImg.getWidth();
int imgHeight = foreImg.getHeight();
double scale = Math.min(widthSize * 1.0 / imgWidth, heightSize * 1.0 / imgHeight);
width = (int) (imgWidth * scale) + getPaddingLeft() + getPaddingRight();
height = (int) (imgHeight * scale) + getPaddingTop() + getPaddingBottom();
} else { // 如果没有设置前景图片
width = widthMode == MeasureSpec.EXACTLY ? widthSize : (int) (textWidth + getPaddingLeft() + getPaddingRight());
height = heightMode == MeasureSpec.EXACTLY ? heightSize : textSize + getPaddingTop() + getPaddingBottom();
}
setMeasuredDimension(width, height);
} /**
* 说明:正常情况下,我们不需要在继承自View的自定义View中写onLayout()方法
* 但是由于onMeasure()方法在运行时会调用多次,因此我们把一些无关操作放到onLayout()中
* 最终目的是避免一些操作执行多次影响整体性能
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
foreBm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bmPixels = new int[foreBm.getWidth() * foreBm.getHeight()];
foreCanvas = new Canvas(foreBm);
if (foreImageRes == -1) {
// 如果不设置前景图片,则默认用灰色覆盖
foreCanvas.drawColor(Color.GRAY);
} else {
foreImg = zoomBitmap(foreImg, width, height);
foreCanvas.drawBitmap(foreImg, 0, 0, null);
}
// 准备绘制刮卡线条的画笔
forePaint = new Paint();
forePaint.setStyle(Paint.Style.STROKE);
forePaint.setStrokeWidth(strokeWidth);
forePaint.setAntiAlias(true);
forePaint.setDither(true);
forePaint.setStrokeCap(Paint.Cap.ROUND);
forePaint.setStrokeJoin(Paint.Join.ROUND);
forePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
path = new Path();
super.onLayout(changed, left, top, right, bottom);
} @Override
protected void onDraw(Canvas canvas) {
// 绘制文本
canvas.drawText(text.toString(), (width - textWidth) / 2, (height + textSize / 2) / 2, textPaint);
// 绘制前景画布的Bitmap
canvas.drawBitmap(foreBm, 0, 0, null);
super.onDraw(canvas);
} @Override
public boolean onTouchEvent(MotionEvent event) {
// 如果所有覆盖物都被清除了,则不响应用户触摸事件
if (!isMaskCleared) {
int currX = (int) event.getX();
int currY = (int) event.getY();
switch (event.getAction()) {
// 当用户按下时,将线条的前端点移动到用户按下的地方,准备绘制
case MotionEvent.ACTION_DOWN:
path.moveTo(currX, currY);
break;
// 当用户滑动时,将线条移动到当前位置,进行绘制
case MotionEvent.ACTION_MOVE:
path.lineTo(currX, currY);
break;
// 当用户抬起手指时,判断消除的面积是否达到一定的阈值,如果达到则清除所有覆盖物
case MotionEvent.ACTION_UP:
int blankPx = 0;
foreBm.getPixels(bmPixels, 0, width, 0, 0, width, height);
for (int bmPixel : bmPixels) {
if (bmPixel == 0) {
blankPx++;
}
}
if (blankPx * 1.0 / bmPixels.length >= uncoverFraction) {
for (int i = 0; i < bmPixels.length; i++) {
bmPixels[i] = 0;
}
foreBm.setPixels(bmPixels, 0, width, 0, 0, width, height);
isMaskCleared = true;
listener.onGuaguakaUncovered(text.toString());
}
break;
}
// 绘制线条,请求重绘整个控件
foreCanvas.drawPath(path, forePaint);
invalidate();
}
return true;
} /**
* 设置刮刮卡View显示的文本
*/
public void setText(String text) {
this.text.delete(0, this.text.length());
this.text.append(text);
} /**
* 设置刮刮卡View显示的文本的颜色
*/
public void setTextColor(int textColor) {
this.textColor = textColor;
} /**
* 将指定图片缩放到指定宽高,返回新的图片Bitmap对象
*/
public static Bitmap zoomBitmap(Bitmap bm, int newWidth, int newHeight) {
// 获得图片的宽高
int width = bm.getWidth();
int height = bm.getHeight();
// 计算缩放比例
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// 取得想要缩放的matrix参数
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
// 得到新的图片
return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
} /**
* 刮刮卡的回调接口
*/
interface OnGuaguakaUncoverListener {
// 当所有覆盖物都被清除后,回调这个方法
void onGuaguakaUncovered(String text);
} /**
* 为刮刮卡View设置Listener
*/
public void setOnGuaguakaUncoverListener(OnGuaguakaUncoverListener listener) {
this.listener = listener;
}
}
自定义属性文件 /res/values/attr.xml 中的代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="foreImage" format="reference" /> <!-- 前景图片 -->
<attr name="text" format="string" /> <!-- 奖励文本 -->
<attr name="textSize" format="dimension" /> <!-- 文本字体大小 -->
<attr name="textColor" format="color" /> <!-- 文本颜色 -->
<attr name="uncoverFraction" format="float" /> <!-- 刮卡阈值,达到这个阈值后自动清除所有覆盖物 -->
<attr name="strokeWidth" format="dimension" /> <!-- 刮卡线条的粗细 --> <declare-styleable name="GuaguakaView">
<attr name="foreImage" />
<attr name="text" />
<attr name="textSize" />
<attr name="textColor" />
<attr name="uncoverFraction" />
<attr name="strokeWidth" />
</declare-styleable>
</resources>
主界面布局文件 activity_main.xml 中的代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"> <my.itgungnir.custom_guaguaka.GuaguakaView
android:id="@+id/guaguaka_main_ggk_ggk"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
app:foreImage="@mipmap/foreground"
app:strokeWidth="20.0dip"
app:textSize="20.0sp"
app:uncoverFraction="0.6" /> </RelativeLayout>
主界面JAVA文件 MainActivity.java 中的代码:
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast; public class MainActivity extends AppCompatActivity {
private GuaguakaView ggk; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ggk = (GuaguakaView) findViewById(R.id.guaguaka_main_ggk_ggk); int r = (int) (Math.random() * 10000);
if (r != 0 && r % 2 == 0) {
ggk.setText("$" + r);
ggk.setTextColor(Color.RED);
} else {
ggk.setText("谢谢惠顾");
ggk.setTextColor(Color.BLACK);
} ggk.setOnGuaguakaUncoverListener(new GuaguakaView.OnGuaguakaUncoverListener() {
@Override
public void onGuaguakaUncovered(String text) {
if ("谢谢惠顾".equals(text)) {
Toast.makeText(MainActivity.this, "很遗憾,没有中奖", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "恭喜!中奖" + text + "!", Toast.LENGTH_SHORT).show();
}
}
});
}
}
项目的运行效果图如下所示:

【Android - 自定义View】之自定义View实现“刮刮卡”效果的更多相关文章
- Android 自定义View修炼-【2014年最后的分享啦】Android实现自定义刮刮卡效果View
一.简介: 今天是2014年最后一天啦,首先在这里,我祝福大家在新的2015年都一个个的新健康,新收入,新顺利,新如意!!! 上一偏,我介绍了用Xfermode实现自定义圆角和椭圆图片view的博文& ...
- Android XML中引用自定义内部类view的四个why
今天碰到了在XML中应用以内部类形式定义的自定义view,结果遇到了一些坑.虽然通过看了一些前辈写的文章解决了这个问题,但是我看到的几篇都没有完整说清楚why,于是决定做这个总结. 使用自定义内部类v ...
- android 最简单的自定义圆点view
首先创建一个选择器,用来判断圆点状态,可以根本自己的需求改 <selector xmlns:android="http://schemas.android.com/apk/res/an ...
- Android 高手进阶之自定义View,自定义属性(带进度的圆形进度条)
Android 高手进阶(21) 版权声明:本文为博主原创文章,未经博主允许不得转载. 转载请注明地址:http://blog.csdn.net/xiaanming/article/detail ...
- Android 自定义View修炼-打造完美的自定义侧滑菜单/侧滑View控件
一.概述 在App中,经常会出现侧滑菜单,侧滑滑出View等效果,虽然说Android有很多第三方开源库,但是实际上 咱们可以自己也写一个自定义的侧滑View控件,其实不难,主要涉及到以下几个要点: ...
- Android艺术探索第四 view的自定义
一.初见View View的层级关系(Veiw到底分成几层,自定义view是从那一层开始绘制的) R:Veiw树的结构如下 ,自定义View是从DecorView开始的;DecorView是View树 ...
- Android查缺补漏(View篇)--自定义 View 的基本流程
View是Android很重要的一部分,常用的View有Button.TextView.EditView.ListView.GridView.各种layout等等,开发者通过对这些View的各种组合以 ...
- 【Android 应用开发】自定义View 和 ViewGroup
一. 自定义View介绍 自定义View时, 继承View基类, 并实现其中的一些方法. (1) ~ (2) 方法与构造相关 (3) ~ (5) 方法与组件大小位置相关 (6) ~ (9) 方法与触摸 ...
- Android自定义View前传-View的三大流程-Measure
Android自定义View前传-View的三大流程-Measure 参考 <Android开发艺术探索> https://developer.android.google.cn/refe ...
随机推荐
- Shiro笔记---授权
1.搭建shiro环境(*) idea2018.2.maven3.5.4.jdk1.8 项目结构: pom.xml: <?xml version="1.0" encoding ...
- C++智能指针类型转换
#include <iostream> #include <memory> struct Base { int a; virtual void f() const { std: ...
- 《Effective Java》 读书笔记(七)消除过期的对象引用
大概看了一遍这个小节,其实这种感觉体验最多的应该是C/C++程序,有多杀少个new就得有多个delete. 一直以为Java就不会存在这个问题,看来是我太年轻. 感觉<Effective Jav ...
- JS 接口定义及实现的例子
//定义一个函数,目的是将参数中的第二个函数所有属性放到第一个参数中,目的是将接口中所有方法放到实现类中 Object.extend=function(destination,source){ for ...
- Asp.net Core 系列之--3.领域、仓储、服务简单实现
ChuanGoing 2019-11-11 距离上篇近两个月时间,一方面时因为其他事情耽搁,另一方面也是之前准备不足,关于领域驱动有几个地方没有想通透,也就没有继续码字.目前网络包括园子里大多领域驱 ...
- Java实现不遍历数组求和
package com.jts.t1; /** * 不遍历数组求和 * 方法省略异常检查 */ public class Demo1 { public static void main(String[ ...
- 基于 JavaFX 开发的聊天客户端 OIM-即时通讯
OIM 详细介绍 一.简介 OIM是一套即时通讯的聊天系统,在这里献给大家,一方面希望能够帮助对即时通讯有兴趣研究的朋友,希望我们能够共同进步,另一个就是希望能够帮助到需要即时通讯系统的朋友或者企业, ...
- 裸板中中断异常处理,linux中断异常处理 ,linux系统中断处理的API,中断处理函数的要求,内核中登记底半部的方式
1.linux系统中的中断处理 1.0裸板中中断异常是如何处理的? 以s5p6818+按键为例 1)按键中断的触发 中断源级配置 管脚功 ...
- 算法编程题积累(4)——腾讯笔试"有趣的数字“问题
本题基本思路:先对原序列进行排序,再根据不同情况采用不同算法. 首先差最大的对数最好求:用最小的数的个数 × 最大的数的个数即可. 接着求差最小的对数: 1.当序列中无重复关键字时:可知最小差必然产生 ...
- 实现支持多用户在线的FTP程序(C/S)
1. 需求 1. 用户加密认证 2. 允许多用户登录 3. 每个用户都有自己的家目录,且只能访问自己的家目录 4. 对用户进行磁盘分配,每一个用户的可用空间可以自己设置 5. 允许用户在ftp ser ...