首先来介绍一下这个自定义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 中的代码:

  1. import android.content.Context;
  2. import android.content.res.TypedArray;
  3. import android.graphics.Bitmap;
  4. import android.graphics.BitmapFactory;
  5. import android.graphics.Canvas;
  6. import android.graphics.Color;
  7. import android.graphics.Matrix;
  8. import android.graphics.Paint;
  9. import android.graphics.Path;
  10. import android.graphics.PorterDuff;
  11. import android.graphics.PorterDuffXfermode;
  12. import android.support.annotation.Nullable;
  13. import android.util.AttributeSet;
  14. import android.util.TypedValue;
  15. import android.view.MotionEvent;
  16. import android.view.View;
  17.  
  18. /**
  19. * 自定义“刮刮卡”View
  20. */
  21. public class GuaguakaView extends View {
  22. private int width, height; // 刮刮卡布局最终显示的宽度和高度
  23.  
  24. private int foreImageRes = -1; // 自定义属性:前景图片
  25. private StringBuffer text = new StringBuffer(); // 自定义属性:显示的文本
  26. private int textSize = -1; // 自定义属性:文本字体大小
  27. private int textColor = Color.BLACK; // 自定义属性:文本颜色
  28. private float uncoverFraction = 0.6f; // 自定义属性:当刮开多少比重的时候消除所有覆盖物
  29. private int strokeWidth = -1; // 自定义属性:刮卡时的线条粗细
  30.  
  31. private Canvas foreCanvas; // 前景画布,用于绘制前景色、前景图片和刮卡线条
  32. private Paint forePaint; // 用于绘制前景色、前景图片和刮卡线条的画笔
  33. private Paint textPaint; // 用于绘制文本的画笔
  34. private Bitmap foreBm; // 前景画布中的Bitmap对象
  35. private Bitmap foreImg; // 前景图片的Bitmap对象
  36. private Path path; // 刮卡线条
  37. private int[] bmPixels; // 保存前景中所有像素的数组
  38.  
  39. private boolean isMaskCleared; // 记录前景是否都被消除了
  40. private float textWidth; // 文本的宽度
  41.  
  42. private OnGuaguakaUncoverListener listener; // 回调接口
  43.  
  44. public GuaguakaView(Context context) {
  45. this(context, null);
  46. }
  47.  
  48. public GuaguakaView(Context context, @Nullable AttributeSet attrs) {
  49. this(context, attrs, 0);
  50. }
  51.  
  52. public GuaguakaView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  53. super(context, attrs, defStyleAttr);
  54. // 加载自定义属性
  55. TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.GuaguakaView, defStyleAttr, 0);
  56. int attrCount = array.getIndexCount();
  57. for (int i = 0; i < attrCount; i++) {
  58. int attr = array.getIndex(i);
  59. switch (attr) {
  60. case R.styleable.GuaguakaView_foreImage:
  61. foreImageRes = array.getResourceId(attr, -1);
  62. break;
  63. case R.styleable.GuaguakaView_text:
  64. text.delete(0, text.length());
  65. text.append(array.getString(attr));
  66. break;
  67. case R.styleable.GuaguakaView_textSize:
  68. textSize = (int) TypedValue.applyDimension(
  69. TypedValue.COMPLEX_UNIT_SP,
  70. array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, context.getResources().getDisplayMetrics())),
  71. context.getResources().getDisplayMetrics());
  72. break;
  73. case R.styleable.GuaguakaView_textColor:
  74. textColor = array.getColor(attr, Color.BLACK);
  75. break;
  76. case R.styleable.GuaguakaView_uncoverFraction:
  77. uncoverFraction = array.getFloat(attr, 0.6f);
  78. break;
  79. case R.styleable.GuaguakaView_strokeWidth:
  80. strokeWidth = (int) TypedValue.applyDimension(
  81. TypedValue.COMPLEX_UNIT_DIP,
  82. array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics())),
  83. context.getResources().getDisplayMetrics());
  84. break;
  85. }
  86. }
  87. array.recycle();
  88. // 设置一些初始值
  89. if (textSize == -1) {
  90. textSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, context.getResources().getDisplayMetrics());
  91. }
  92. if (strokeWidth == -1) {
  93. strokeWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());
  94. }
  95. if (foreImageRes != -1) {
  96. foreImg = BitmapFactory.decodeResource(getResources(), foreImageRes);
  97. }
  98. }
  99.  
  100. @Override
  101. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  102. int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  103. int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  104. int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  105. int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  106. textPaint = new Paint();
  107. textPaint.setColor(textColor);
  108. textPaint.setTextSize(textSize);
  109. textWidth = textPaint.measureText(text.toString());
  110. // 如果设置了前景图片,则按照图片的宽高比例铺满父布局提供的宽高
  111. if (foreImageRes != -1) {
  112. int imgWidth = foreImg.getWidth();
  113. int imgHeight = foreImg.getHeight();
  114. double scale = Math.min(widthSize * 1.0 / imgWidth, heightSize * 1.0 / imgHeight);
  115. width = (int) (imgWidth * scale) + getPaddingLeft() + getPaddingRight();
  116. height = (int) (imgHeight * scale) + getPaddingTop() + getPaddingBottom();
  117. } else { // 如果没有设置前景图片
  118. width = widthMode == MeasureSpec.EXACTLY ? widthSize : (int) (textWidth + getPaddingLeft() + getPaddingRight());
  119. height = heightMode == MeasureSpec.EXACTLY ? heightSize : textSize + getPaddingTop() + getPaddingBottom();
  120. }
  121. setMeasuredDimension(width, height);
  122. }
  123.  
  124. /**
  125. * 说明:正常情况下,我们不需要在继承自View的自定义View中写onLayout()方法
  126. * 但是由于onMeasure()方法在运行时会调用多次,因此我们把一些无关操作放到onLayout()中
  127. * 最终目的是避免一些操作执行多次影响整体性能
  128. */
  129. @Override
  130. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  131. foreBm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
  132. bmPixels = new int[foreBm.getWidth() * foreBm.getHeight()];
  133. foreCanvas = new Canvas(foreBm);
  134. if (foreImageRes == -1) {
  135. // 如果不设置前景图片,则默认用灰色覆盖
  136. foreCanvas.drawColor(Color.GRAY);
  137. } else {
  138. foreImg = zoomBitmap(foreImg, width, height);
  139. foreCanvas.drawBitmap(foreImg, 0, 0, null);
  140. }
  141. // 准备绘制刮卡线条的画笔
  142. forePaint = new Paint();
  143. forePaint.setStyle(Paint.Style.STROKE);
  144. forePaint.setStrokeWidth(strokeWidth);
  145. forePaint.setAntiAlias(true);
  146. forePaint.setDither(true);
  147. forePaint.setStrokeCap(Paint.Cap.ROUND);
  148. forePaint.setStrokeJoin(Paint.Join.ROUND);
  149. forePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
  150. path = new Path();
  151. super.onLayout(changed, left, top, right, bottom);
  152. }
  153.  
  154. @Override
  155. protected void onDraw(Canvas canvas) {
  156. // 绘制文本
  157. canvas.drawText(text.toString(), (width - textWidth) / 2, (height + textSize / 2) / 2, textPaint);
  158. // 绘制前景画布的Bitmap
  159. canvas.drawBitmap(foreBm, 0, 0, null);
  160. super.onDraw(canvas);
  161. }
  162.  
  163. @Override
  164. public boolean onTouchEvent(MotionEvent event) {
  165. // 如果所有覆盖物都被清除了,则不响应用户触摸事件
  166. if (!isMaskCleared) {
  167. int currX = (int) event.getX();
  168. int currY = (int) event.getY();
  169. switch (event.getAction()) {
  170. // 当用户按下时,将线条的前端点移动到用户按下的地方,准备绘制
  171. case MotionEvent.ACTION_DOWN:
  172. path.moveTo(currX, currY);
  173. break;
  174. // 当用户滑动时,将线条移动到当前位置,进行绘制
  175. case MotionEvent.ACTION_MOVE:
  176. path.lineTo(currX, currY);
  177. break;
  178. // 当用户抬起手指时,判断消除的面积是否达到一定的阈值,如果达到则清除所有覆盖物
  179. case MotionEvent.ACTION_UP:
  180. int blankPx = 0;
  181. foreBm.getPixels(bmPixels, 0, width, 0, 0, width, height);
  182. for (int bmPixel : bmPixels) {
  183. if (bmPixel == 0) {
  184. blankPx++;
  185. }
  186. }
  187. if (blankPx * 1.0 / bmPixels.length >= uncoverFraction) {
  188. for (int i = 0; i < bmPixels.length; i++) {
  189. bmPixels[i] = 0;
  190. }
  191. foreBm.setPixels(bmPixels, 0, width, 0, 0, width, height);
  192. isMaskCleared = true;
  193. listener.onGuaguakaUncovered(text.toString());
  194. }
  195. break;
  196. }
  197. // 绘制线条,请求重绘整个控件
  198. foreCanvas.drawPath(path, forePaint);
  199. invalidate();
  200. }
  201. return true;
  202. }
  203.  
  204. /**
  205. * 设置刮刮卡View显示的文本
  206. */
  207. public void setText(String text) {
  208. this.text.delete(0, this.text.length());
  209. this.text.append(text);
  210. }
  211.  
  212. /**
  213. * 设置刮刮卡View显示的文本的颜色
  214. */
  215. public void setTextColor(int textColor) {
  216. this.textColor = textColor;
  217. }
  218.  
  219. /**
  220. * 将指定图片缩放到指定宽高,返回新的图片Bitmap对象
  221. */
  222. public static Bitmap zoomBitmap(Bitmap bm, int newWidth, int newHeight) {
  223. // 获得图片的宽高
  224. int width = bm.getWidth();
  225. int height = bm.getHeight();
  226. // 计算缩放比例
  227. float scaleWidth = ((float) newWidth) / width;
  228. float scaleHeight = ((float) newHeight) / height;
  229. // 取得想要缩放的matrix参数
  230. Matrix matrix = new Matrix();
  231. matrix.postScale(scaleWidth, scaleHeight);
  232. // 得到新的图片
  233. return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
  234. }
  235.  
  236. /**
  237. * 刮刮卡的回调接口
  238. */
  239. interface OnGuaguakaUncoverListener {
  240. // 当所有覆盖物都被清除后,回调这个方法
  241. void onGuaguakaUncovered(String text);
  242. }
  243.  
  244. /**
  245. * 为刮刮卡View设置Listener
  246. */
  247. public void setOnGuaguakaUncoverListener(OnGuaguakaUncoverListener listener) {
  248. this.listener = listener;
  249. }
  250. }

  自定义属性文件 /res/values/attr.xml 中的代码:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3. <attr name="foreImage" format="reference" /> <!-- 前景图片 -->
  4. <attr name="text" format="string" /> <!-- 奖励文本 -->
  5. <attr name="textSize" format="dimension" /> <!-- 文本字体大小 -->
  6. <attr name="textColor" format="color" /> <!-- 文本颜色 -->
  7. <attr name="uncoverFraction" format="float" /> <!-- 刮卡阈值,达到这个阈值后自动清除所有覆盖物 -->
  8. <attr name="strokeWidth" format="dimension" /> <!-- 刮卡线条的粗细 -->
  9.  
  10. <declare-styleable name="GuaguakaView">
  11. <attr name="foreImage" />
  12. <attr name="text" />
  13. <attr name="textSize" />
  14. <attr name="textColor" />
  15. <attr name="uncoverFraction" />
  16. <attr name="strokeWidth" />
  17. </declare-styleable>
  18. </resources>

  主界面布局文件 activity_main.xml 中的代码:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent">
  6.  
  7. <my.itgungnir.custom_guaguaka.GuaguakaView
  8. android:id="@+id/guaguaka_main_ggk_ggk"
  9. android:layout_width="match_parent"
  10. android:layout_height="match_parent"
  11. android:layout_centerInParent="true"
  12. app:foreImage="@mipmap/foreground"
  13. app:strokeWidth="20.0dip"
  14. app:textSize="20.0sp"
  15. app:uncoverFraction="0.6" />
  16.  
  17. </RelativeLayout>

  主界面JAVA文件 MainActivity.java 中的代码:

  1. import android.graphics.Color;
  2. import android.support.v7.app.AppCompatActivity;
  3. import android.os.Bundle;
  4. import android.widget.Toast;
  5.  
  6. public class MainActivity extends AppCompatActivity {
  7. private GuaguakaView ggk;
  8.  
  9. @Override
  10. protected void onCreate(Bundle savedInstanceState) {
  11. super.onCreate(savedInstanceState);
  12. setContentView(R.layout.activity_main);
  13. ggk = (GuaguakaView) findViewById(R.id.guaguaka_main_ggk_ggk);
  14.  
  15. int r = (int) (Math.random() * 10000);
  16. if (r != 0 && r % 2 == 0) {
  17. ggk.setText("$" + r);
  18. ggk.setTextColor(Color.RED);
  19. } else {
  20. ggk.setText("谢谢惠顾");
  21. ggk.setTextColor(Color.BLACK);
  22. }
  23.  
  24. ggk.setOnGuaguakaUncoverListener(new GuaguakaView.OnGuaguakaUncoverListener() {
  25. @Override
  26. public void onGuaguakaUncovered(String text) {
  27. if ("谢谢惠顾".equals(text)) {
  28. Toast.makeText(MainActivity.this, "很遗憾,没有中奖", Toast.LENGTH_SHORT).show();
  29. } else {
  30. Toast.makeText(MainActivity.this, "恭喜!中奖" + text + "!", Toast.LENGTH_SHORT).show();
  31. }
  32. }
  33. });
  34. }
  35. }

  项目的运行效果图如下所示:

【Android - 自定义View】之自定义View实现“刮刮卡”效果的更多相关文章

  1. Android 自定义View修炼-【2014年最后的分享啦】Android实现自定义刮刮卡效果View

    一.简介: 今天是2014年最后一天啦,首先在这里,我祝福大家在新的2015年都一个个的新健康,新收入,新顺利,新如意!!! 上一偏,我介绍了用Xfermode实现自定义圆角和椭圆图片view的博文& ...

  2. Android XML中引用自定义内部类view的四个why

    今天碰到了在XML中应用以内部类形式定义的自定义view,结果遇到了一些坑.虽然通过看了一些前辈写的文章解决了这个问题,但是我看到的几篇都没有完整说清楚why,于是决定做这个总结. 使用自定义内部类v ...

  3. android 最简单的自定义圆点view

    首先创建一个选择器,用来判断圆点状态,可以根本自己的需求改 <selector xmlns:android="http://schemas.android.com/apk/res/an ...

  4. Android 高手进阶之自定义View,自定义属性(带进度的圆形进度条)

      Android 高手进阶(21)  版权声明:本文为博主原创文章,未经博主允许不得转载. 转载请注明地址:http://blog.csdn.net/xiaanming/article/detail ...

  5. Android 自定义View修炼-打造完美的自定义侧滑菜单/侧滑View控件

    一.概述 在App中,经常会出现侧滑菜单,侧滑滑出View等效果,虽然说Android有很多第三方开源库,但是实际上 咱们可以自己也写一个自定义的侧滑View控件,其实不难,主要涉及到以下几个要点: ...

  6. Android艺术探索第四 view的自定义

    一.初见View View的层级关系(Veiw到底分成几层,自定义view是从那一层开始绘制的) R:Veiw树的结构如下 ,自定义View是从DecorView开始的;DecorView是View树 ...

  7. Android查缺补漏(View篇)--自定义 View 的基本流程

    View是Android很重要的一部分,常用的View有Button.TextView.EditView.ListView.GridView.各种layout等等,开发者通过对这些View的各种组合以 ...

  8. 【Android 应用开发】自定义View 和 ViewGroup

    一. 自定义View介绍 自定义View时, 继承View基类, 并实现其中的一些方法. (1) ~ (2) 方法与构造相关 (3) ~ (5) 方法与组件大小位置相关 (6) ~ (9) 方法与触摸 ...

  9. Android自定义View前传-View的三大流程-Measure

    Android自定义View前传-View的三大流程-Measure 参考 <Android开发艺术探索> https://developer.android.google.cn/refe ...

随机推荐

  1. 数据文件包解析工具类 RandomAccessFile

    public class ReadTextFile { public static void main(String[] args) { pic2txt(); parseFrmFile(); //ur ...

  2. H5 + WebGL 实现的楼宇自控 3D 可视化监控

    前言 智慧楼宇和人们的生活息息相关,楼宇智能化程度的提高,会极大程度的改善人们的生活品质,在当前工业互联网大背景下受到很大关注.目前智慧楼宇可视化监控的主要优点包括: 智慧化 -- 智慧楼宇是一个生态 ...

  3. [考试反思]1011csp-s模拟测试69:无常

    承蒙大脸skyh的毒奶,加之以被kx和Parisb以及板儿逼剥夺了一中午的睡眠(其实还有半个晚上)RP守恒终于失效了,连续两场没考好 RP也是不够了,竟然考原题,而且还不换题,连样例都一模一样只不过加 ...

  4. Android 开源库 GitHub 托管

    本文微信公众号「AndroidTraveler」首发. 背景 之前给大家写过一篇文章 Android 上传开源项目到 jcenter 实战踩坑之路,分享了上传开源项目到 jcenter 上面的一些踩坑 ...

  5. 「BZOJ1576」[Usaco2009 Jan] 安全路经Travel------------------------P2934 [USACO09JAN]安全出行Safe Travel

    原题地址 题目描述 Gremlins have infested the farm. These nasty, ugly fairy-like creatures thwart the cows as ...

  6. 期末考试(正解:三分单峰函数 me~)

    好久没有水过杂题了! 今天lsc终于刚过了三道考试题来水杂题了! 期末考试 首先一看还是一脸mb(这是正常现象,毕竟我不像一些大神可以一眼出正解)然后我就被颓了标签,知道是三分单峰函数,但是自己实在是 ...

  7. 『图论』LCA最近公共祖先

    概述篇 LCA(Least Common Ancestors),即最近公共祖先,是指这样的一个问题:在一棵有根树中,找出某两个节点 u 和 v 最近的公共祖先. LCA可分为在线算法与离线算法 在线算 ...

  8. day7-集合

    一.定义变量是为了吹处理状态的变化,定义变量名是为了获取变量值.字符串.数字.列表.元组.字典都是为了更好的描述变量的状态1.可变不可变:变量名不变时,里面内容是否可以变化# 可变:列表.字典.修改变 ...

  9. vue的相关知识

    一.DOM vs 函数库 vs框架 DOM:  API繁琐 函数库:JQuery对DOM的每个步骤的API进行一对一的简化,但并没有改变DOM做事的步骤和方法. 框架:一个包含部分已经实现的功能的半成 ...

  10. [视频演示].NET Core开发的iNeuOS物联网平台,实现从设备&PLC、云平台、移动APP数据链路闭环

    目       录 1.      概述... 1 2.      登陆信息... 2 3.      设备驱动... 3 4.      组态建模... 3 5.      手机APP. 5 6.  ...