ScratchView:一步步打造万能的 Android 刮奖后果控件

时间:2017-12-04 18:48:45 来源:
默认
特大
宋体
黑体
雅黑
楷体

原题目:ScratchView:一步步打造万能的 Android 刮奖效果控件

Hello,大家好,我是Clock。这周为大家带来一篇关于自定义控件的文章,这也是我本人第一次写关于自定义控件的文章,盼望可以写得要言不烦,通熟易懂点。

前言

我身边有一部门开发的小搭档,存在着这样一种习惯。某一天,忽然看到某一款 App 上有个很美丽的自定义控件(动画)效果,就会绞尽头脑想措施去自己实现一发。当然,我自己也是属于这类型的骚年,看到某种效果就会手痒难耐揣摩着实现套路。个人感到这是一种需求驱动提高的方法,当你绞尽脑子去实现自己想要的效果时,你就会发明你对 Android 自定义控件(动画)的常识系统意识越深,长此以往,自己也能轻松的造出各种控件(动画)效果。要是哪天,产品童鞋拿着个原型(或者对着某款 App )跟你讲:“XXXX,你看这个效果我们能不能实现?”,然后你瞥了一眼,胸有成竹丢回一句:“开玩笑,还有我实现不了的效果?”。想想心里是不是有点小冲动?好了,差未几要说回正题了,这是我第一篇关于自定义控件的文章,以也会陆续交叉更新此类型的文章,愿望大家可能爱好。(偷偷剧透下,我下篇文章是关于机能优化的干货。当然我本人认为很干货,生机到时候发出来不要打脸,哈哈哈!)

实现效果

说了这么多,仍是先给大家看看最终的实现效果先。

上面只是基础实现后果的一局部,你会看到下方还有良多其余控件,它们是用来干嘛的,接下来行将为你揭晓所有。

根本实现

日常生涯中,我们对刮奖效果想必不会生疏,其原理就是通过在原有图案和文字上添加刮层来实现的,用比双11更低低低低低的价钱,领有Blueair家用智能空气污染器!。如果我们想看到刮层后面藏的图案和文字是什么,势必要通过刮开刮层才行。知道了这样的套路,就可以开始收拾一下编码实现思路,然后愉快开干。

我一开始的实现思路是想通过重写 ImageView 和 TextView ,而后在分辨用代码在图像和文字上添加图层,这样的话就能实现出效果了。然而回首一想,错误,这种实现存在的局限性比拟大。如果照这种思路实现,那么刮层下面只能存在图片或者文字,如果产品经理要求同时存在图片和文字呢?请求存在两张图片呢?要求同时存在图片和文字,且文字放在图片的上(下、左、右)呢?…咱们都知道,世界上最善变的除了妹纸的心,就是产品经理和他们的需要了。于是,便想出另外一种实现思路,直接继续 View 来实现一个刮层,让这个刮层和图片以及文字不发生任何依附,再结合 FrameLayout 将刮层放置最上一层,刮层之下你想放多少图片文字,图片文字要怎么布局摆放都行。到此,思路明白,可以高兴的开始编码了。

第一步:绘制出刮层效果。

package com.clock.scratch;import ...;

/** * Created by Clock on 2016/8/26. */

public classScratchView extends View { ... public ScratchView(Context context) { super(context); TypedArray typedArray = context.obtainStyledAttributes(R.styleable.ScratchView); init(typedArray); } ... private voidinit(TypedArray typedArray) { ... mMaskColor = typedArray.getColor(R.styleable.ScratchView_maskColor, DEFAULT_MASKER_COLOR); mMaskPaint = newPaint(); mMaskPaint.setAntiAlias( true); //抗锯齿mMaskPaint.setDither( true); //防抖setMaskColor(mMaskColor); ... }

/** * 设置蒙板颜色 * * @param color 十六进制颜色值,如:0xffff0000(不透明的红色) */

public voidsetMaskColor(int color) { mMaskPaint.setColor(color); } @Override protected voidonDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawBitmap(mMaskBitmap, 0, 0, mBitmapPaint); //绘制图层遮罩} @Override protected voidonSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); createMasker(w, h); }

/** * 创建蒙层 * * @param width * @param height */private voidcreateMasker(int width, int height) { mMaskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mMaskCanvas = newCanvas(mMaskBitmap); Rect rect = newRect( 0, 0, width, height); mMaskCanvas.drawRect(rect, mMaskPaint); //绘制生成和控件大小一致的遮罩 Bitmap}}<?xml version= "1.0"encoding= "utf8"?> <resources><declarestyleablename="ScratchView"><!蒙层的颜色><attrname="maskColor"format="color|reference"/></declarestyleable></resources>

上面的代码思路如下:

创建出继承于 View 的自定义控件 ScratchView,同时在init() 函数中初始化各类参数设置。如刮层的颜色等等;

为了便利设置,需要把参数抽离成控件的自定义属性,同时 ScratchView 类中提供 set 办法,供代码调用。如刮层的颜色属性就是 maskColor ,其在类中对应的方法就是 setMaskColor;

在 onSizeChanged 中,利用 View 已经 Measure 结束,可以取得 View 的宽高,并应用 Canvas 来初始化生成 mMaskBitmap 用于制造刮层;

在 onDraw 中,利用 canvas.drawBitmap 将 onSizeChanged 中初始化生成 mMaskBitmap 绘制显示到界面,生成刮层;

在 Demo 中增添如下布局,看下效果:

<FrameLayout

android:layout_width= "200dp"

android:layout_height= "200dp"

android:layout_gravity= "center_horizontal"

android:layout_marginTop= "8dp">

<!刮层下遮住的内容>

<ImageView

android:layout_width="150dp"

android:layout_height="150dp"

android:layout_gravity="center"

android:src="@mipmap/lufy"/>

<!刮层>

<com.clock.scratch.ScratchView

android:id="@+id/scratch_view"

android:layout_width="match_parent"

android:layout_height="match_parent"/>

</FrameLayout>

到此,我们已经获得了一个刮层的实现效果,同时可以直接在 xml 布局和 java 代码中设置刮层的颜色了。然而这时候,只是空有刮层,并不实现刮开的效果,接下来继承添加实现代码。

第二步:实现刮开效果。

package com.clock.scratch;import ...;public classScratchView extends View { public ScratchView(Context context) { super(context); TypedArray typedArray = context.obtainStyledAttributes(R.styleable.ScratchView); init(typedArray); } private voidinit(TypedArray typedArray) { mEraseSize = typedArray.getFloat(R.styleable.ScratchView_eraseSize, DEFAULT_ERASER_SIZE); ... mErasePaint = newPaint(); mErasePaint.setAntiAlias( true); mErasePaint.setDither( true); mErasePaint.setXfermode( newPorterDuffXfermode(PorterDuff.Mode.CLEAR)); //设置擦除效果mErasePaint.setStyle(Paint.Style.STROKE); mErasePaint.setStrokeCap(Paint.Cap.ROUND); //设置笔尖外形,让绘制的边缘油滑setEraserSize(mEraseSize); mErasePath = newPath(); ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); mTouchSlop = viewConfiguration.getScaledTouchSlop(); }

/** * 设置橡皮檫尺寸大小(默认大小是 60) * * @param eraserSize 橡皮檫尺寸大小 */public voidsetEraserSize(float eraserSize) { mErasePaint.setStrokeWidth(eraserSize); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction();

switch(action) {

caseMotionEvent.ACTION_DOWN: startErase(event.getX(), event.getY()); invalidate();

returntrue;

caseMotionEvent.ACTION_MOVE: erase(event.getX(), event.getY()); invalidate();

returntrue;

caseMotionEvent.ACTION_UP: stopErase(); invalidate();

returntrue;

default:

break; }

returnsuper.onTouchEvent(event); }

/** * 开端擦除 * * @param x * @param y */private voidstartErase(float x, float y) { mErasePath.reset(); mErasePath.moveTo(x, y);

this.mStartX = x;

this.mStartY = y; }

/** * 擦除 * * @param x * @param y */private voiderase(float x, float y) { int dx = (int) Math.abs(x mStartX); int dy = (int) Math.abs(y mStartY);

if(dx >= mTouchSlop || dy >= mTouchSlop) {

this.mStartX = x;

this.mStartY = y; mErasePath.lineTo(x, y); mMaskCanvas.drawPath(mErasePath, mErasePaint); mErasePath.reset(); mErasePath.moveTo(mStartX,揭开日本糖尿病治愈之谜!, mStartY); } }

/** * 结束擦除 */private voidstopErase() {

this.mStartX = 0;

this.mStartY = 0; mErasePath.reset(); }} <?xml version= "1.0"encoding= "utf8",美军特务机器人,水陆两用,将替换侦察兵?> <resources>

<declarestyleablename="ScratchView">

<!擦除尺寸大小>

<attrname="eraseSize"format="float"/>

</declarestyleable></resources>

上面的代码思路如下:

在 init() 中初始化 mErasePaint 和 mErasePath ,并设置 mErasePaint 的 Xfermode 为 PorterDuff.Mode.CLEAR 用于后面制作出刮奖效果;

重写 onTouchEvent 函数,处置触摸事件 ACTION_DOWN 、 ACTION_MOVE 、 ACTION_UP 等三种事件类型,并应用 mErasePath 记载手指滑动轨迹,再用 mMaskCanvas 将滑动轨迹绘制到第一步生成的 mMaskBitmap 上 ,最后通过调用 invalidate() 引起 View 的重绘天生刮开效果;

为了避免滑动过于敏锐,我们需要对滑动做一个判断就是通过体系提供的 viewConfiguration.getScaledTouchSlop() 获取系统认为的最小滑动间隔,当等于或者超过这个距离时,才认为是在滑动,这就是为什么我在 erase() 要加 dx >= mTouchSlop || dy >= mTouchSlop 的判断;

为了把持刮痕的粗细,跟前面设置刮层的色彩一样,同样为 ScratchView 自定义一个属性 eraseSize 实当初 xml 中节制。同时,在 Java 代码中供给调用方式;

到此,一个基本的刮奖效果已经完成了,我们来看看实现效果如何。

以上两步仅仅实现基本效果罢了了,接下来我们来做一些优化。

效果优化

第一步优化:增加水印

许多刮奖的效果都会有在刮层上添加自家 logo 做水印效果(这里不知道称为水印适合吗?反正就是大略那个意思)。如下面的支付宝一样

我们在基础实现的第一步中的创建刮层函数里面添加实古代码,同时也添加一个自定义属性和 set 方法可供调用:

/** * 设置水印图标 * * @param resId 图标资源id,1表现去除水印 */public voidsetWatermark(int resId) {

if(resId == 1) { mWatermark = null; } else{ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId); mWatermark = newBitmapDrawable(bitmap); mWatermark.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); } }

/** * 创立蒙层 * * @param width * @param height */private voidcreateMasker(int width, int height) { ...

if(mWatermark != null) { //Rect bounds = newRect(rect); mWatermark.setBounds(bounds); mWatermark.draw(mMaskCanvas); } }

实现效果如下:

当然,像效果上还有很多可以进行添加,例如还可以加上面支付宝的那种边沿锯齿效果等等,这里就各位童鞋自行脑洞实现啦。

第二步优化:添加相应事件监听器,以及完美一些常用函数。

说到事件监听,我想这里莫过于刮奖完成的事件了吧。对使用这个控件的开发者,确定需要在刮完之后做相应的操作,例如,提醒用户中奖啦,还是持续尽力之类的。怎么样判定刮奖完成呢?这里的实现思路是通过异步盘算刮层 mMaskBitmap 中的像素信息值,通过算得透明像素个数占总像素个数的比例,当这个比例超过一定阈值的时候,我们以为刮奖完成了。为什么要说超过必定阈值就算完成,这和事实生活中刮奖一样,你不需要把刮层完整刮得干清洁净才干得到成果。当然这个比例是多少,我们同样需要抽离成可动态设置的。再添加监听器接口和设置监听器的 API 即可。实现代码,大抵如下:

private voidonErase() { int width = getWidth(); int height = getHeight();

newAsyncTask<Integer, Integer, Boolean>() { @Override protected BooleandoInBackground(Integer... params) { int width = params[ 0]; int height = params[ 1]; int pixels[] = newint[width * height]; mMaskBitmap.getPixels(pixels, 0, width, 0, 0, width, height); //获取笼罩图层中所有的像素信息,stride用于表示一行的像素个数有多少float erasePixelCount = 0; //擦除的像素个数float totalPixelCount = width * height; //总像素个数for(int pos = 0; pos < totalPixelCount; pos++) { if(pixels[pos] == 0) { //透明的像素值为0erasePixelCount++; } } int percent = 0;

if(erasePixelCount >= 0&& totalPixelCount > 0) { percent = Math.round(erasePixelCount * 100/ totalPixelCount); publishProgress(percent); }

returnpercent >= mMaxPercent; } @Override protected voidonProgressUpdate(Integer... values) { super.onProgressUpdate(values); mPercent = values[ 0]; onPercentUpdate(); } @Override protected voidonPostExecute( Booleanresult) { super.onPostExecute(result);

if(result && !mIsCompleted) { //标志擦除,并完成回调mIsCompleted = true;

if(mEraseStatusListener != null) { mEraseStatusListener.onCompleted(ScratchView.this); } } } }.execute(width, height); }

/** * 设置擦除监听器 * * @param listener */public voidsetEraseStatusListener(EraseStatusListener listener) {

this.mEraseStatusListener = listener; }

/** * 擦除状况监听器 */public static interface EraseStatusListener {

/** * 擦除进度 * * @param percent 进度值,大于0,小于即是100; */public voidonProgress(int percent);

/** * 擦除完成回调函数 * * @param view */public voidonCompleted(View view); }

我们来看看终极效果

到这里,一个完全的刮奖效果自定义控件实现已经完成。不外,这里还有一个问题需要抛给大家独特思考下,就是在断定刮奖是否完成的实现上,我在代码中的实现方法会创建出大批的 int 数组,这样造成成果就是会产生内存抖动。

目前,由于我自己也没想到什么好计划,所以,大家假如有好的思路,不妨在下方留言赐教一下。

总结

第一次写自定义控件这类型的文章,不晓得大家看清楚实现思路了吗?对于自定义控件,单看文章只能懂其中的思路,联合源代码边着手实际调试再加上文章会更深有领会。须要源代码的童鞋能够到 https://github.com/Dclock/ScratchView 中下载。

来自:极客头条 http://geek.csdn.net/news/detail/99435

程序员共读整顿宣布,转载请接洽作者失掉受权

Copyright 2012-2018 www.ctv0123.com 版权所有 关于我们 | 广告服务 | 诚聘英才 | 联系我们 | 友情链接 | 免责申明
友情链接:幸运飞艇人工计划  幸运飞艇助赢软件  幸运飞艇冠亚军玩法  幸运飞艇开奖时间  幸运飞艇稳赚  

免责声明: 本站资料及图片来源互联网文章,本网不承担任何由内容信息所引起的争议和法律责任。所有作品版权归原创作者所有,与本站立场无关,如用户分享不慎侵犯了您的权益,请联系我们告知,我们将做删除处理!