Android - View之自定义View实现“刮刮卡”效果

归志宁无五亩园,读书本意在元元。这篇文章主要讲述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之自定义View实现“刮刮卡”效果】 

    推荐阅读