zoukankan      html  css  js  c++  java
  • 【Android

      首先来介绍一下这个自定义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();
                    }
                }
            });
        }
    }

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

  • 相关阅读:
    一百一十:CMS系统之剩余菜单栏的页面和视图
    一百零九:CMS系统之前端根据不同权限渲染不同菜单
    一百零八:CMS系统之封装权限判断功能
    一百零七:CMS系统之权限和角色模型定义
    一百零六:CMS系统之修改邮箱功能完成
    一百零五:CMS系统之flask-mail使用和邮箱配置、发送邮件功能
    一百零四:CMS系统之修改邮箱界面
    一百零三:CMS系统之使用sweetalert提示框优化返回结果
    一百零二:CMS系统之sweetalert提示框和使用
    一百零一:CMS系统之自定义restful风格json返回格式和内容
  • 原文地址:https://www.cnblogs.com/itgungnir/p/6747551.html
Copyright © 2011-2022 走看看