GVKun编程网logo

Android 自定义View之随机数验证码(仿写鸿洋),写给即将正在找工作的Android攻城狮

22

在这篇文章中,我们将带领您了解Android自定义View之随机数验证码的全貌,包括仿写鸿洋,写给即将正在找工作的Android攻城狮的相关情况。同时,我们还将为您介绍有关Android自定义View

在这篇文章中,我们将带领您了解Android 自定义View之随机数验证码的全貌,包括仿写鸿洋,写给即将正在找工作的Android攻城狮的相关情况。同时,我们还将为您介绍有关Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解、Android 自定义View之倒计时实例代码、Android 自定义view之画图板实现方法、Android 自定义View之自定义属性的知识,以帮助您更好地理解这个主题。

本文目录一览:

Android 自定义View之随机数验证码(仿写鸿洋),写给即将正在找工作的Android攻城狮

Android 自定义View之随机数验证码(仿写鸿洋),写给即将正在找工作的Android攻城狮

constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context, attributeSet, defStyle) {

//获取自定义属性

val typedArray = context.theme.obtainStyledAttributes(

attributeSet,

R.styleable.RandomTextView,

defStyle,

0

)

mRandomText = typedArray.getString(R.styleable.RandomTextView_randomText).toString()

mRandomTextColor = typedArray.getColor(R.styleable.RandomTextView_randomTextColor, Color.BLACK)//默认黑色

mRandomTextSize = typedArray.getDimensionPixelSize(

R.styleable.RandomTextView_randomTextSize,

TypedValue.applyDime


nsion( TypedValue.COMPLEX_UNIT_SP, 16F, resources.displayMetrics ).toInt()

)

//获取完回收

typedArray.recycle()

paint.textSize = mRandomTextSize.toFloat()

//返回文本边界,即包含文本的最小矩形,没有所谓“留白”,返回比measureText()更精确的text宽高,数据保存在bounds里

paint.getTextBounds(mRandomText, 0, mRandomText.length, bounds)

}

通过obtainStyledAttributes获取自定义属性,返回一个TypedArray,这里用到了我们在attrs.xml文件中声明的样式(R.styleable.RandomTextView),返回的TypedArray即包含了这里面的属性。

拿到自定义view属性集合,然后赋值,赋值之后就可以用paint去画了。

然后用到了paint的getTextBounds方法:

paint.getTextBounds(mRandomText, 0, mRandomText.length, bounds)

简单理解就是,把文字放在一个矩形里,通过矩形的宽高即可知道文字的宽高,所以宽高会保存在bounds里,bounds是一个矩形Rect,为什么要这么做呢,因为后面我们要计算文字居中的时候会用到。

ok,接下来开始画布局。

4.重写onDraw计算坐标绘制

@SuppressLint(“DrawAllocation”)

override fun onDraw(canvas: Canvas?) {

super.onDraw(canvas)

/**

  • 自定义view时,需要我们自己在onDraw中处理padding,否则是不生效的

  • 自定义viewGroup时,子view的padding放在onMeasure中处理

*/

/**

  • 矩形背景

*/

paint.color = Color.YELLOW

//计算坐标,因为原点是在文字的左下角,左边要是延伸出去就还要往左边去,所以是减,右边和下边是正,所以是加

canvas?.drawRect(

(0 - paddingLeft).toFloat(),

(0 - paddingTop).toFloat(),

(measuredWidth + paddingRight).toFloat(),

(measuredHeight + paddingBottom).toFloat(),

paint

)

/**

  • 文本

*/

paint.color = mRandomTextColor

//注意这里的坐标xy不是左上角,而是左下角,所以高度是相加的,在自定义view中,坐标轴右下为正

//getWidth 等于 measuredWidth

canvas?.drawText(

mRandomText,

(width / 2 - bounds.width() / 2).toFloat(),

(height / 2 + bounds.height() / 2).toFloat(),

paint

)

}

上面的代码就是在onDraw里面显示绘制了一个YELLOW颜色的矩形背景,然后绘制了一个自定义属性颜色的居中的文本。

这里要注意我们计算位置时的坐标,在自定义view中,原点是view的左上角,而在数学坐标系中,原点(0,0)是在中间的,二者是有区别的。

其次,假如xml布局中有padding,或者预判会使用到padding,在重写onDraw的时候也要把padding的数据加上,否则padding是不生效的。如果是继承ViewGroup时,子view的padding放在onMeasure中处理。

来看此时的效果:

在这里插入图片描述

此时是不是有疑惑,xml里面的宽高明明是wrap_content,为什么会充满父布局呢?

这就涉及到onMeasure的知识点了,往下看。

5.重写onMeasure测量宽高

我们在xml设置view宽高有3种方式:

  • match_parent

  • wrap_content

  • 具体数据,比如100dp

onMeasureMeasureSpecmode也有3种模式:

  • EXACTLY:一般是设置了明确的值或者是MATCH_PARENT

  • AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

  • UNSPECIFIED:表示子布局想要多大就多大,很少使用

由于我们xml用的是wrap_content,也就是对应AT_MOST,所以效果就是会占满父布局中的可用空间,而父布局是填充屏幕,所以我们自定义的view也会占满全屏。

而我们实际想要的效果是view包裹自己,而不是铺满全屏,所以我们需要在onMeasure中进行处理

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec)

/**

  • EXACTLY:一般是设置了明确的值或者是MATCH_PARENT

  • AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

  • UNSPECIFIED:表示子布局想要多大就多大,很少使用

*/

val widthMode = MeasureSpec.getMode(widthMeasureSpec)

val widthSize = MeasureSpec.getSize(widthMeasureSpec)

val heightMode = MeasureSpec.getMode(heightMeasureSpec)

val heightSize = MeasureSpec.getSize(heightMeasureSpec)

var width = 0

var height = 0

//如果指定了宽度,或不限制宽度,用可用宽度即可,如果是WARP_CONTENT,则用文本宽度,再加上左右padding

when (widthMode) {

MeasureSpec.UNSPECIFIED,

MeasureSpec.EXACTLY -> {

width = widthSize + paddingLeft + paddingRight

}

MeasureSpec.AT_MOST -> {

width = bounds.width() + paddingLeft + paddingRight

}

}

//如果指定了高度,或不限制高度,用可用高度即可,如果是WARP_CONTENT,则用文本高度,再加上上下padding

when (heightMode) {

MeasureSpec.UNSPECIFIED,

MeasureSpec.EXACTLY -> {

height = heightSize + paddingTop + paddingBottom

}

MeasureSpec.AT_MOST -> {

height = bounds.height() + paddingTop + paddingBottom

}

}

//保存测量的宽高

setMeasuredDimension(width, height)

}

上面的代码呢,主要做了两件事:

  • 获取view宽高的模式

  • 针对不同的模式,对宽高进行重新测量

最后别忘记调用setMeasuredDimension保存新测量的宽高,否则没用哦。

此时再看效果就是效果图中的样子了。

6.设置点击事件

ok,到这,view已经绘制完成了,但是还没有事件,我们在构造中加一个点击事件

constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context, attributeSet, defStyle) {

/**

  • 添加点击事件

*/

this.setonClickListener {

mRandomText = randomText()

//更新

postInvalidate()

}

}

randomText方法:

/**

  • 根据文本长度 随意数字

*/

private fun randomText(): String {

val list = mutablelistof()

for (index in mRandomText.indices) {

list.add(Random.nextInt(10))

}

val stringBuffer = StringBuffer()

for (i in list) {

stringBuffer.append("" + i)

}

return stringBuffer.toString()

}

触发事件之后,文字更新,然后view重绘更新页面即可。

关于数据获取,也就是变化后的数字,可以写个onTextChanged接口,也可以写个开放方法获取。

总结

=============================================================

其实看效果的话,还不如TextView来的简单,而且TextView也可以轻松的实现效果图中的效果。
Text.indices) {

list.add(Random.nextInt(10))

}

val stringBuffer = StringBuffer()

for (i in list) {

stringBuffer.append("" + i)

}

return stringBuffer.toString()

}

触发事件之后,文字更新,然后view重绘更新页面即可。

关于数据获取,也就是变化后的数字,可以写个onTextChanged接口,也可以写个开放方法获取。

总结

=============================================================

其实看效果的话,还不如TextView来的简单,而且TextView也可以轻松的实现效果图中的效果。

Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解

Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解

View 的绘制系列文章:

  • Android View 绘制流程之 DecorView 与 ViewRootImpl

  • Android View 的绘制流程之 Measure 过程详解 (一)

  • Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)

  • Android View 的事件分发原理解析

  • Android 自定义 View 详解

对于 Android 开发者来说,原生控件往往无法满足要求,需要开发者自定义一些控件,因此,需要去了解自定义 view 的实现原理。这样即使碰到需要自定义控件的时候,也可以游刃有余。

基础知识

自定义 View 分类

自定义 View 的实现方式有以下几种:

类型 定义 自定义组合控件 多个控件组合成为一个新的控件,方便多处复用 继承系统 View 控件 继承自TextView等系统控件,在系统控件的基础功能上进行扩展 继承 View 不复用系统控件逻辑,继承View进行功能定义 继承系统 ViewGroup 继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展 继承 View ViewGroup 不复用系统控件逻辑,继承ViewGroup进行功能定义

从上到下越来越难,需要的了解的知识也是越来越多的。

构造函数

当我们在自定义 View 的时候,构造函数都是不可缺少,需要对构造函数进行重写,构造函数有多个,至少要重写其中一个才行。例如我们新建 MyTextView:

   
public class MyTextView extends View {
  /** * 在java代码里new的时候会用到 * @param context */ public MyTextView(Context context) { super(context); } /** * 在xml布局文件中使用时自动调用 * MyTextView(Context context,@Nullable AttributeSet attrs) { (context,attrs); } * 不会自动调用,如果有默认style时,在第二个构造函数中调用 * context * attrs * defStyleAttr public MyTextView(Context context,@Nullable AttributeSet attrs,int defStyleAttr) { * 只有在API版本>21时才会用到 * 不会自动调用,如果有默认style时,在第二个构造函数中调用 * defStyleAttr * defStyleRes */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) int defStyleAttr,1)"> defStyleRes) { 函数的作用,都已经再代码里面写出来了。

自定义属性

写过布局的同学都知道,系统控件的属性在 xml 中都是以 android 开头的。对于自定义 View,也可以自定义属性,在 xml 中使用。

Android 自定义属性可分为以下几步:

  1. 自定义一个 View

  2. 编写 values/attrs.xml,在其中编写 styleable 和 item 等标签元素

  3. 在布局文件中 View 使用自定义的属性(注意 namespace)

  4. 在 View 的构造方法中通过 TypedArray 获取

e.g  还是以上面的 MyTextView 做演示:

首先我在 activity_main.xml 中引入了 MyTextView:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height
    tools:context=".MainActivity">

    com.example.myapplication.MyTextView
        android:layout_width="100dp"
        android:layout_height="200dp"
        app:testAttr="520"
        app:text="helloWorld" />

</android.support.constraint.ConstraintLayout>

然后我在 values/attrs.xml 中添加自定义属性:

resources>
    declare-styleable name="test">
        attr ="text" format="string" />
        ="testAttr"="integer" />
    declare-styleable>
>

记得在构造函数里面说过,xml 布局会调用第二个构造函数,因此在这个构造函数里面获取属性和解析:

    context.obtainStyledAttributes(attrs,R.styleable.test);
        int textAttr = ta.getInteger(R.styleable.test_testAttr,-1);
        String text = ta.getString(R.styleable.test_text);
        Log.d(TAG," text = " + text + ",textAttr = " + textAttr);
     // toast 显示获取的属性值 Toast.makeText(context,text
+ " " + textAttr,Toast.LENGTH_LONG).show(); ta.recycle(); }

注意当你在引用自定义属性的时候,记得加上 name 前缀,否则会引用不到。

这里本想截图 log 的,奈何就是不显示,就搞成 toast 了。

当然,你还可以自定义很多其他属性,包括 color,string, integer,boolean,flag,甚至是混合等。

自定义组合控件

自定义组合控件就是将多个控件组合成为一个新的控件,主要解决多次重复使用同一类型的布局。如我们顶部的 HeaderView 以及 dailog 等,我们都可以把他们组合成一个新的控件。

我们通过一个自定义 MyView1 实例来了解自定义组合控件的用法。

xml 布局 

merge ="wrap_content"="wrap_content">
    
    TextView
        android:id="@+id/Feed_item_com_cont_title"
        android:layout_width
        android:ellipsize="end"
        android:includeFontPadding="false"
        android:maxLines="2"
        android:text="title" />

    ="@+id/Feed_item_com_cont_desc"
        android:layout_below="@id/Feed_item_com_cont_title"="desc" merge>

 自定义 View 代码 :

package com.example.myapplication;

import android.content.Context;
 android.util.AttributeSet;
 android.view.LayoutInflater;
 android.view.View;
 android.widget.RelativeLayout;
 android.widget.TextView;

class MyView1 extends RelativeLayout {

     标题 private TextView mTitle;
     描述  TextView mDesc;

     MyView1(Context context) {
        this(context,1)">null);
    }

     MyView1(Context context,AttributeSet attrs) {
        public MyView1(Context context,AttributeSet attrs,defStyleAttr);
        initView(context);
    }

    
     * 初使化界面视图
     *
     *  context 上下文环境
     protected void initView(Context context) {
        View rootView = LayoutInflater.from(getContext()).inflate(R.layout.my_view1,1)">this);

        mDesc = rootView.findViewById(R.id.Feed_item_com_cont_desc);
        mTitle = rootView.findViewById(R.id.Feed_item_com_cont_title);
    }
}

在布局当中引用该控件 

LinearLayout 
    android:orientation="vertical"="@+id/text"
        android:clickable="true"
        android:enabled
        android:focusable="trsfnjsfksjfnjsdfjksdhfjksdjkfhdsfsdddddddddddddddddddddddddd" ="@+id/myview"com.example.myapplication.MyView1
        ="wrap_content" LinearLayout>

最终效果如下图所示 :

 

继承系统控件

继承系统的控件可以分为继承 View子类(如 TextView 等)和继承 ViewGroup 子类(如 LinearLayout 等),根据业务需求的不同,实现的方式也会有比较大的差异。这里介绍一个比较简单的,继承自View的实现方式。

业务需求:为文字设置背景,并在布局中间添加一条横线。

因为这种实现方式会复用系统的逻辑,大多数情况下我们希望复用系统的 onMeaseur 和 onLayout 流程,所以我们只需要重写 onDraw 方法 。实现非常简单,话不多说,直接上代码。

 android.graphics.Canvas;
 android.graphics.LinearGradient;
 android.graphics.Shader;
 android.text.TextPaint;
 android.widget.TextView;


import static android.support.v4.content.ContextCompat.getColor;


 * 包含分割线的textView
 * 文字左右两边有一条渐变的分割线
 * 样式如下:
 * ———————— 文字 ————————
 */
class DividingLineTextView  TextView {
     线性渐变  LinearGradient mLinearGradient;
     textPaint  TextPaint mPaint;
     文字 private String mText = "";
     屏幕宽度 private  mScreenWidth;
     开始颜色  mStartColor;
     结束颜色  mEndColor;
     字体大小  mTextSize;


    
     * 构造函数
     public DividingLineTextView(Context context,1)"> defStyle) {
         getResources().getDimensionPixelSize(R.dimen.text_size);
        mScreenWidth = getCalculateWidth(getContext());
        mStartColor = getColor(getContext(),R.color.colorAccent);
        mEndColor =new LinearGradient(0,mScreenWidth,1)">,new []{mStartColor,mEndColor,mStartColor},1)">float[]{0,0.5fnew TextPaint();
    }

     DividingLineTextView(Context context,1)"> DividingLineTextView(Context context) {
        );
    }

    @Override
     onDraw(Canvas canvas) {
        .onDraw(canvas);
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
        int len = getTextLength(mText,mPaint);
        // 文字绘制起始坐标
        int sx = mScreenWidth / 2 - len / 2;
         文字绘制结束坐标
        int ex = mScreenWidth / 2 + len / 2int height = getMeasuredHeight();
        mPaint.setShader(mLinearGradient);
         绘制左边分界线,从左边开始:左边距15dp, 右边距距离文字15dp
        canvas.drawLine(mTextSize,height / 2,sx - mTextSize,height / 2 绘制右边分界线,从文字右边开始:左边距距离文字15dp,右边距15dp
        canvas.drawLine(ex + mTextSize,mScreenWidth - mTextSize,mPaint);
    }

    
     * 返回指定文字的宽度,单位px
     *
     *  str   要测量的文字
     *  paint 绘制此文字的画笔
     * @return 返回文字的宽度,单位px
      getTextLength(String str,TextPaint paint) {
        return () paint.measureText(str);
    }

    
     * 更新文字
     *
     *  text 文字
      update(String text) {
        mText = text;
        setText(mText);
         刷新重绘
        requestLayout();
    }


    
     * 获取需要计算的宽度,取屏幕高宽较小值,
     *
     *  context context
     *  屏幕宽度值
     static  getCalculateWidth(Context context) {
         context.getResources().getdisplayMetrics().heightPixels;
         动态屏幕宽度,在折叠屏手机上宽度在分屏时会发生变化
        int Width = context.getResources().getdisplayMetrics().widthPixels;

        return Math.min(Width,height);
    }
}

对于 View 的绘制还需要对 Paint()canvas 以及 Path 的使用有所了解,不清楚的可以稍微了解一下。 

看下布局里面的引用:

xml 布局 

>

   // ...... 跟前面一样忽视
    com.example.myapplication.DividingLineTextView
        ="@+id/divide"
        android:gravity="center" >

 

activty 里面代码如下 :
   onCreate(Bundle savedInstanceState) {
        .onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DividingLineTextView te = findViewById(R.id.divide);
        te.update("DividingLineTextView");
  }

这里通过 update() 对来重新绘制,确保边线在文字的两边。视觉效果如下:

 

直接继承View

直接继承 View 会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑,除了要重写 onDraw 外还需要对 onMeasure 方法进行重写。

我们用自定义 View 来绘制一个正方形。

首先定义构造方法,以及做一些初始化操作

ublic class RectView  View{
    定义画笔
    private Paint mPaint =  Paint();

    
     * 实现构造方法
     *  RectView(Context context) {
        (context);
        init();
    }

     RectView(Context context,attrs);
        init();
    }

    public RectView(Context context,defStyleAttr);
        init();
    }

     init() {
        mPaint.setColor(Color.BLUE);

    }

}

 重写 draw 方法,绘制正方形,注意对 padding 属性进行设置:


     * 重写draw方法
     *  canvas
     
    @Override
    .onDraw(canvas);
        获取各个编剧的padding值
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        获取绘制的View的宽度
        int width = getWidth()-paddingLeft-paddingRight;
        获取绘制的View的高度
        int height = getHeight()-paddingTop-paddingBottom;
        绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
        canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);
    }

在 View 的源码当中并没有对 AT_MOST 和 EXACTLY 两个模式做出区分,也就是说 View 在 wrap_content 和 match_parent 两个模式下是完全相同的,都会是 match_parent,显然这与我们平时用的 View 不同,所以我们要重写 onMeasure 方法。

    
     * 重写onMeasure方法
     *
     *  widthMeasureSpec
     *  heightMeasureSpec
     void onMeasure(int widthMeasureSpec,1)"> heightMeasureSpec) {
        .onMeasure(widthMeasureSpec,heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        处理wrap_contentde情况
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300,300);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize,300);
        }
    }

 最终效果如图所示:

可以发现,我们设置的是 wrap_content,但是最后还是有尺寸的。

整个过程大致如下,直接继承 View 时需要有几点注意:

  1. 在 onDraw 当中对 padding 属性进行处理。

  2. 在 onMeasure 过程中对 wrap_content 属性进行处理。

  3. 至少要有一个构造方法。

继承ViewGroup

自定义 ViewGroup 的过程相对复杂一些,因为除了要对自身的大小和位置进行测量之外,还需要对子 View 的测量参数负责。

需求实例

实现一个类似于 Viewpager 的可左右滑动的布局。

布局文件:

com.example.myapplication.MyHorizonView
        
        android:background="@color/colorAccent"="400dp">

        ListView
            ="@+id/list1"
            android:layout_width
            android:layout_height
            android:background="@color/colorAccent" />

        ="@+id/list2"="@color/colorPrimary" ="@+id/list3"="@color/colorPrimaryDark" com.example.myapplication.MyHorizonView="1dp"="2dp"com.example.myapplication.RectView
        />


>

一个 ViewGroup 里面放入 3 个 ListView,注意 ViewGroup 设置的宽是 wrap_conten,在测量的时候,会对 wrap_content 设置成与父 View 的大小一致,具体实现逻辑可看后面的代码。

代码比较多,我们结合注释分析。

class MyHorizonView  ViewGroup {

    final String TAG = "HorizontaiView"private List<View> mMatchedChildrenList = new ArrayList<>();


     MyHorizonView(Context context) {
         MyHorizonView(Context context,AttributeSet attributes) {
        public MyHorizonView(Context context,AttributeSet attributes,attributes,defStyleAttr);
    }

    @Override
    void onLayout(boolean changed,1)">int l,1)">int t,1)">int r,1)"> b) {
        int childCount = getChildCount();
        int left = 0;
        View child;
        for (int i = 0; i < childCount; i++) {
            child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                int childWidth = child.getMeasuredWidth();
                 因为是水平滑动的,所以以宽度来适配
                child.layout(left,left + childWidth,child.getMeasuredHeight());
                left += childWidth;
            }
        }
    }

    @Override
    int widthSpecMode =int widthSpecsize =int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecsize = 如果不是确定的的值,说明是 AT_MOST,与父 View 同宽高
        final boolean measureMatchParentChildren = heightSpecMode != MeasureSpec.EXACTLY ||
                widthSpecMode != MeasureSpec.EXACTLY;
         getChildCount();
        View child;
        final LayoutParams layoutParams = child.getLayoutParams();
                measureChild(child,widthMeasureSpec,heightMeasureSpec);
                if (measureMatchParentChildren) {
                     需要先计算出父 View 的高度来再来测量子 view
                    if (layoutParams.width == LayoutParams.MATCH_PARENT
                            || layoutParams.height == LayoutParams.MATCH_PARENT) {
                        mMatchedChildrenList.add(child);
                    }
                }
            }
        }

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
             如果宽高都是AT_MOST的话,即都是wrap_content布局模式,就用View自己想要的宽高值
            setMeasuredDimension(getMeasuredWidth(),getMeasuredHeight());
        } if (widthSpecMode == 如果只有宽度都是AT_MOST的话,即只有宽度是wrap_content布局模式,宽度就用View自己想要的宽度值,高度就用父ViewGroup指定的高度值
pecsize);
        } if (heightSpecMode == 如果只有高度都是AT_MOST的话,即只有高度是wrap_content布局模式,高度就用View自己想要的宽度值,宽度就用父ViewGroup指定的高度值
            setMeasuredDimension(widthSpecsize,getMeasuredHeight());
        }

        int i = 0; i < mMatchedChildrenList.size(); i++) {
            View matchChild =if (matchChild.getVisibility() != matchChild.getLayoutParams();
                 计算子 View 宽的 MeasureSpec
                 childWidthMeasureSpec;
                 LayoutParams.MATCH_PARENT) {
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,layoutParams.width);
                }
                 计算子 View 高的 MeasureSpec
                 childHeightMeasureSpec;
                if (layoutParams.height == LayoutParams.MATCH_PARENT) {
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),1)"> {
                    childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec,layoutParams.height);
                }
                 根据 MeasureSpec 计算自己的宽高
                matchChild.measure(childWidthMeasureSpec,childHeightMeasureSpec);
            }
        }
    }
}

这里我们只是重写了两个绘制过程中的重要的方法:onMeasure 和 onLayout 方法。

对于 onMeasure 方法具体逻辑如下:

  1. super.onMeasure 会先计算自定义 view 的大小;

  2. 调用 measureChild 对 子 View 进行测量;
  3. 自定义 view 设置的宽高参数不是 MeasureSpec.EXACTLY 的话,对于子 View 是 match_parent 需要额外处理,同时也需要对 MeasureSpec.AT_MOST 情况进行额外处理。

  4.  当自定义view 的大小确定后,在对子 View 是 match_parent 重新测量;

上述的测量过程的代码也是参考 FrameLayout 源码的,具体可以参看文章:

对于 onLayout 方法,因为是水平滑动的,所以要根据宽度来进行layout。

到这里我们的 View 布局就已经基本结束了。但是要实现 Viewpager 的效果,还需要添加对事件的处理。事件的处理流程之前我们有分析过,在制作自定义 View 的时候也是会经常用到的,不了解的可以参考文章 Android Touch事件分发超详细解析。

  init(Context context) {
        mScroller =  Scroller(context);
        mTracker = VeLocityTracker.obtain();
    }

    
     * 因为我们定义的是ViewGroup,从onInterceptTouchEvent开始。
     * 重写onInterceptTouchEvent,对横向滑动事件进行拦截
     *
     *  event
     * @return
     boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = falseint x = () event.getX();
        int y = () event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;必须不能拦截,否则后续的ACTION_MOME和ACTION_UP事件都会拦截。
                break;
             MotionEvent.ACTION_MOVE:
                intercepted = Math.abs(x - mLastX) > Math.abs(y - mLastY);
                ;
        }
        Log.d(TAG,"onInterceptTouchEvent: intercepted " + intercepted);
        mLastX = x;
        mLastY = y;
        return intercepted ? intercepted : .onInterceptHoverEvent(event);
    }

    
     * 当ViewGroup拦截下用户的横向滑动事件以后,后续的Touch事件将交付给`onTouchEvent`进行处理。
     * 重写onTouchEvent方法
      onTouchEvent(MotionEvent event) {
        mTracker.addMovement(event);
         MotionEvent.ACTION_DOWN:
                 MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                Log.d(TAG,"onTouchEvent: deltaX " + deltaX);

                 scrollBy 方法将对我们当前 View 的位置进行偏移
                scrollBy(-deltaX,1)">);
                 MotionEvent.ACTION_UP:
                Log.d(TAG,"onTouchEvent: " + getScrollX());
                 getScrollX()为在X轴方向发生的便宜,mChildWidth * currentIndex表示当前View在滑动开始之前的X坐标
                 distance存储的就是此次滑动的距离
                int distance = getScrollX() - mChildWidth * mCurrentIndex;
                当本次滑动距离>View宽度的1/2时,切换View
                if (Math.abs(distance) > mChildWidth / 2) {
                    if (distance > 0) {
                        mCurrentIndex++;
                    }  {
                        mCurrentIndex--;
                    }
                }  {
                    获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点
                    mTracker.computeCurrentVeLocity(1000);
                    float xV = mTracker.getXVeLocity();
                    当X轴加速度>50时,也就是产生了快速滑动,也会切换View
                    if (Math.abs(xV) > 50) {
                        if (xV < 0) {
                            mCurrentIndex++;
                        }  {
                            mCurrentIndex--;
                        }
                    }
                }

                对currentIndex做出限制其范围为【0,getChildCount() - 1】
                mCurrentIndex = mCurrentIndex < 0 ? 0 : mCurrentIndex > getChildCount() - 1 ? getChildCount() - 1 : mCurrentIndex;
                滑动到下一个View
                smoothScrollTo(mCurrentIndex * mChildWidth,1)">);
                mTracker.clear();

                ;
        }

        Log.d(TAG,"onTouchEvent: ");
        mLastX =return .onTouchEvent(event);
    }

    @Override
     dispatchTouchEvent(MotionEvent ev) {
        .dispatchTouchEvent(ev);
    }

    void smoothScrollTo(int destX,1)"> destY) {
         startScroll方法将产生一系列偏移量,从(getScrollX(),getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离
        mScroller.startScroll(getScrollX(),getScrollY(),destX - getScrollX(),destY - getScrollY(),1000);
         invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法
        invalidate();
    }

     重写computeScroll方法
    @Override
     computeScroll() {
        .computeScroll();
         当scroller.computeScrollOffset()=true时表示滑动没有结束
         (mScroller.computeScrollOffset()) {
             调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
             没有滑动结束,继续刷新View
            postInvalidate();
        }
    }

具体效果如下图所示:


对于 Scroller 的用法总结如下:

  1. 调用 Scroller 的 startScroll() 方法来进行一些滚动的初始化设置,然后迫使 View 进行绘制 (调用 View 的 invalidate() 或 postInvalidate() 就可以重新绘制 View);

  2. 绘制 View 的时候 drawchild 方法会调用 computeScroll() 方法,重写 computeScroll(),通过 Scroller 的 computeScrollOffset() 方法来判断滚动有没有结束;

  3. scrollTo() 方法虽然会重新绘制 View,但还是要调用下 invalidate() 或者 postInvalidate() 来触发界面重绘,重新绘制 View 又触发 computeScroll();

  4. 如此往复进入一个循环阶段,即可达到平滑滚动的效果;

也许有人会问,干嘛还要调用来调用去最后在调用 scrollTo() 方法,还不如直接调用 scrollTo() 方法来实现滚动,其实直接调用是可以,只不过 scrollTo() 是瞬间滚动的,给人的用户体验不太好,所以 Android 提供了 Scroller 类实现平滑滚动的效果。

为了方面大家理解,我画了一个简单的调用示意图:

 

 

到此,自定义 view 的方法就讲完了。希望对大家有用。

参考文献:

1、Android自定义View全解

总结

以上是小编为你收集整理的Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解全部内容。

如果觉得小编网站内容还不错,欢迎将小编网站推荐给好友。

Android 自定义View之倒计时实例代码

Android 自定义View之倒计时实例代码

Android 自定义view之倒计时实例代码

需求:

大多数app在注册的时候,都有一个获取验证码的按钮,点击后,访问接口,最终用户会收到短信验证码。为了不多次写这个获取验证码的接口,下面将它自定义成一个view,方便使用。

分析一下,这是一个TextView,点击的时候变色,不能再点击,同时里面的倒计时开始显示。那么就有了下面的代码

代码:

/** 
 * 通过selector选择器来改变背景,其中倒计时运行时为android:state_enabled="true", 
 * 不显示倒计时时为android:state_enabled="false"; 
 * */ 
public class CountDownView extends TextView { 
   
  private long totalMills = 10 * 1000;//倒计时的总时间,根据需要更改这个值 
  private long interval = 1000;//倒计时的时间间隔 
   
  public CountDownView(Context context) { 
    super(context); 
  } 
 
  public CountDownView(Context context,AttributeSet attrs) { 
    super(context,attrs); 
  } 
 
  public CountDownView(Context context,AttributeSet attrs,int defStyle) { 
    super(context,attrs,defStyle); 
  } 
 
  private TimeCount mTimeCount; 
   
  private void startCount(long totalMills,long countDownInterval) { 
    if (mTimeCount == null) 
      mTimeCount = new TimeCount(totalMills,countDownInterval); 
    mTimeCount.start(); 
  } 
   
  public void start(){ 
    defaultText = getText().toString(); 
    startCount(totalMills,interval); 
  } 
 
  public void cancel() { 
    if (mTimeCount != null){ 
      mTimeCount.onFinish(); 
      mTimeCount.cancel(); 
    } 
  } 
 
  String defaultText = "";//获取到在点击之前的文本内容 
 
  class TimeCount extends CountDownTimer { 
 
    public TimeCount(long millisInFuture,long countDownInterval) { 
      super(millisInFuture,countDownInterval); 
    } 
 
    @Override 
    public void onTick(long millisUntilFinished) { 
      setEnabled(false); 
      setText(millisUntilFinished / 1000 + "S"); 
    } 
 
    @Override 
    public void onFinish() { 
      setEnabled(true); 
      setText(defaultText); 
    } 
 
  } 
 
} 

测试代码:

public class MainActivity extends Activity implements OnClickListener { 
 
  private CountDownView mCountDownView; 
 
  @Override 
  protected void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.activity_main); 
 
    mCountDownView = (CountDownView) findViewById(R.id.tv_1); 
     
    mCountDownView.setonClickListener(this); 
    findViewById(R.id.tv_2).setonClickListener(this); 
  } 
 
  int count = 0; 
  DemoThread thread; 
  @Override 
  public void onClick(View v) { 
     
    if(v.getId() == R.id.tv_1){ 
      mCountDownView.start(); 
      thread = new DemoThread(); 
      thread.start(); 
    } else if (v.getId() == R.id.tv_2) { 
      mCountDownView.cancel(); 
      System.out.println("wisely 取消倒计时"); 
    } 
  } 
   
  class DemoThread extends Thread{ 
    @Override 
    public void run() { 
      while (count < 10) { 
        SystemClock.sleep(100); 
        count++; 
        System.out.println("wisely count:" + count); 
      } 
    } 
  } 
} 

总结:

1、使用的时候,为该控件设置点击事件,然后调用start()方法,剩下的就是你自己的代码,一般都是联网调接口。

2、获取验证码的控件背景可以设置为selector选择器,设置android:state_enabled属性。上面例子的selector选择器代码如下:

<?xml version="1.0" encoding="utf-8"?> 
<selector xmlns:android="http://schemas.android.com/apk/res/android" > 
   
  <item android:state_enabled="true" android:drawable="@color/green"/> 
  <item android:state_enabled="false" android:drawable="@color/red"/> 
 
</selector> 

3、在退出activity时,记得调用控件的cancel方法销毁它,否则会造成内存泄露。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

您可能感兴趣的文章:

  • android自定义倒计时控件示例
  • Android自定义圆形倒计时进度条
  • Android自定义照相机倒计时拍照
  • Android 自定义闪屏页广告倒计时view效果
  • Android自定义View获取注册验证码倒计时按钮
  • Android自定义Chronometer实现短信验证码秒表倒计时功能
  • Android自定义扇形倒计时实例代码
  • Android自定义控件实现验证码倒计时
  • Android自定义View倒计时圆
  • Android使用属性动画如何自定义倒计时控件详解

Android 自定义view之画图板实现方法

Android 自定义view之画图板实现方法

看效果: 中间一个画图板 上方小控件用来显示实时画出的图形 下方小控件用来做一些画图的控制 2个小控件都能移动

顺带还有一个刮刮卡效果,只需要改一个参数:

自定义view首先要自定义属性:

在values下面创建attrs.xml:

 <!--画图板-->
  <declare-styleable name="DrawImg">
    <attr name="PaintColor" />      //画笔颜色
    <attr name="PaintWidth" />      // 画笔宽度
    <attr name="CanvasImg" />      //画板图片
  </declare-styleable>

  <!--指定单位-->
  <attr name="PaintColor" format="color" />    
  <attr name="PaintWidth" format="dimension" />     
  <attr name="CanvasImg" format="reference" />

对于下面3行指定单位的代码可以放出来,可以让多个自定义view 都能使用。

接下来新建自定义view类继承view,重写前3个构造方法

红线标注是android studio 3.0.0对于参数提示的新特性

通过this 让前2个构造方法都实现3个参数的构造方法。
简单说一下构造方法。一个参数的构造方法是在代码中 new 时用到,2个参数的构造方法在布局xml中用到,3个参数的基本就是自定义view类中使用,大概就是这样。

接下来从attrs.xml中通过TypedArray取出自定义属性:

//从attrs文件中取出各个属性
    TypedArray a = context.getTheme().obtainStyledAttributes(attrs,R.styleable.DrawImg,defStyleAttr,0);
    for (int i = 0; i < a.getIndexCount(); i++) {
      int attr = a.getIndex(i);
      switch (attr) {
        case R.styleable.DrawImg_PaintWidth:    //画笔宽度
          paintWidth = a.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(
              TypedValue.COMPLEX_UNIT_DIP,-1,getResources().getdisplayMetrics()));
          break;
        case R.styleable.DrawImg_PaintColor:    //画笔颜色
          paintColor = a.getColor(attr,Color.GREEN);
          break;
        case R.styleable.DrawImg_CanvasImg:     //画板图片
          hasCanvasImg = a.getResourceId(attr,-1);
          break;
      }
    }
    //设置默认画笔宽度
    if (paintWidth == -1) {
      paintWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,20,context.getResources().getdisplayMetrics());
    }
    //取出bitmap
    if (hasCanvasImg != -1) {
      bitmap = BitmapFactory.decodeResource(getResources(),hasCanvasImg);
    }
    //onMeasure可能走多次,onDraw创建对象更不好 所以把画笔路径new在这里
    path = new Path();

需要默认值的设置默认值,以免布局中没有用到自定义属性导致报错。

重写自定义view关键方法onMeasure(),onDraw()。onMeasure()用来指定这个自定义view 的大小,onDraw()用来进行实时绘图

最重要的3个东西:画布Canvas,画笔Paint,路径Path

代码略长但是注释很全,把需要注意的提出来

在newPaint()方法中,paint有一个setXfermode()方法,这个表示图形混合方式,有18种 ~(比下图多了ADD和OVERLAY)~。给张图看一下。这里我们用到2种 SRC_IN和 DST_OUT。

SRC_IN:取两层交集部分,显示上层
DST_OUT:取两层非交集部分,显示下层
说实话这么说也很难懂,还是要自己动手试一试,不过这里只要知道:
使用SRC_IN就会有一个画图板的效果
使用DST_OUT就会有一个刮刮卡的效果

/**
   * onMeasure常见方法
   * 1) getChildCount():获取子View的数量;
   * 2) getChildAt(i):获取第i个子控件;
   * 3) subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
   * 4) measureChild(child,widthMeasureSpec,heightMeasureSpec):测量子View的宽高;
   * 5) child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;
   * 6) getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
   * 7) setMeasuredDimension(width,height):重新设置控件的宽高。如果写了这句代码,就需要删除
   * “super. onMeasure(widthMeasureSpec,heightMeasureSpec);”这行代码。
   */
  @Override
  protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
    /**
     * getMode获取测量模式(下面3种) 和 getSize获取测量值
     *
     * EXACTLY:当宽高值设置为具体值时使用,如100dp、match_parent等,此时取出的size是精确的尺寸;
     * AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的空间;
     * UNSPECIFIED:当没有指定宽高值时使用(很少见)。
     *
     * */
    //测量模式_宽
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    //测量模式_高
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    //宽度
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    //高度
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    //设置view宽度
    //如果布局中给出了准确的宽度,直接使用宽度,否则设置图片宽度为view宽度
    if (widthMode == MeasureSpec.EXACTLY) {
      width = widthSize;
    } else {
      if (hasCanvasImg != -1) {
        //如果设置了图片,使用图片宽
        width = bitmap.getWidth();
      } else {
        //没有设置图片并且也没给准确的view宽高 设置一个宽默认值
        width = 500;
      }
    }
    //设置view高度同上
    if (heightMode == MeasureSpec.EXACTLY) {
      height = heightSize;
    } else {
      if (hasCanvasImg != -1) {
        height = bitmap.getHeight();
      } else {
        height = 500;
      }
    }
    //重新设置view的宽高
    setMeasuredDimension(width,height);

    //设置画布以及画笔
    newPaint();
  }
  private void newPaint() {
    //根据参数创建一个新的bitmap 最后一个参数为为储存形式
    newBitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
    //保存bitmap中所有像素点的数组 
    bmPixels = new int[newBitmap.getWidth() * newBitmap.getHeight()];
    //new带参的Canvas,其中的bitmap参数 必须通过createBitmap得到;
    //否则会报错:IllegalStateException : Immutable bitmap passed to Canvas constructor
    canvas = new Canvas(newBitmap);
    if (hasCanvasImg == -1) {
      //如果没有设置图片,则默认用灰色覆盖
      canvas.drawColor(Color.GRAY);
    } else {
      //把设置的图片缩放到view大小
      bitmap = zoomBitmap(this.bitmap,width,height);
      canvas.drawBitmap(bitmap,null);
    }
    // 准备绘制刮卡线条的画笔
    paint = new Paint();
    paint.setColor(paintColor);
    paint.setStyle(Paint.Style.stroke);
    paint.setstrokeWidth(paintWidth);
    //设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢
    paint.setAntiAlias(true);
    //设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰
    paint.setDither(true);
    //当设置画笔样式为stroke或FILL_OR_stroke时,设置笔刷的图形样式
    paint.setstrokeCap(Paint.Cap.ROUND);
    //设置绘制时各图形的结合方式
    paint.setstrokeJoin(Paint.Join.ROUND);
    //设置图形重叠时的处理方式
    /**
     * SRC_IN:取两层绘制交集。显示上层
     */
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
  }

  //这个onDraw方法只有一句代码,意思是在手指移动的同时把画板图片绘制出来
  @Override
  protected void onDraw(Canvas canvas) {
    canvas.drawBitmap(newBitmap,null);
    super.onDraw(canvas);
  }

  //将指定图片缩放到指定宽高,返回新的图片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,matrix,true);
  }

这是一堆对于这个view来说比较复杂的代码,但是功能很简单,我们做了2件事:

1.通过MeasureSpec.getMode(测量模式),计算出整个控件的宽高
2.通过canvas.drawBitmap在画布上画出bitmap,同时 new 出画笔 Paint 给它设置颜色,粗细等属性

注意:

1.onDraw()方法在每次调用invalidate(),或者视图变化时都会重走,所以不能在里面 new 东西.
2.有一个int[]类型的数组 bmPixels,这里大概说一下是个什么意思,具体的解释在Bitmap类getPixels和createBitmap方法详解中有说道。

bmPixels: 我们通过bitmap的宽度乘以高度,可以的到一个int[]类型的数组,这个数组就是组成bitmap的所有像素点,某一个像素点为0的时候就说明他是没有颜色,!0就说明是有颜色的。

既然是画图,那肯定要监听手指移动,onTouchEvent()方法:

@Override
  public boolean onTouchEvent(MotionEvent event) {
    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:
    }
    // 绘制线条,请求重绘整个控件
    canvas.drawPath(path,paint);
    //请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。
    invalidate();
    return true;
  }

这个就很简单,手指按下时记录位置,path.moveto给path设置起始点位置,移动时通过path.lineto()方法记录路径,同时使用 canvas.drawPath(path,paint)直接绘制出来,invalidate()通知视图更新。

写到这里,在xml布局中使用这个view,已经能画一画了

我们的画笔Paint类,可以指定颜色,粗细,模式,等等,这样我们就可以写一些公开的方法,给它动态的设置这些属性,从而让画笔更加多样性。

//设置画笔颜色
  public void setPaintColor(int color) {
    //path = new Path();
      path.reset();
    paint.setColor(color);
  }

  //设置画笔类型
  public void setPaintMode(int style) {
    //path = new Path();
      path.reset();
    /**
     * SRC_IN:取两层交集部分,显示上层
     * DST_OUT:取两层非交集部分,显示下层
     */
    if (style == 1) {
      paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    } else {
      paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
    }
    resetCanvaas();
  }

  //设置画布重置
  public void resetCanvaas() {
    //path = new Path();
      path.reset();
    canvas.drawBitmap(bitmap,null);
    invalidate();
    listener.bitmapchangelistener(bitmap);
  }

上面代码 设置画笔颜色 ,设置画笔类型以及画布重置为什么都要new Path呢,因为如果不新开一个路径给画笔,当你设置了新的颜色,用的还是以前的Path,画笔就会把以前的Path也重新设置新颜色,而不是保持原来的颜色。

这样就会出现一个问题,每次都在new Path,new一次创建一次,占用一次内存,想到一些避免方法,但是本文画图不是重点,就不在论述。(已改用path.reset())

效果中的右上角,显示了一个float类型的数,它是在刮刮卡模式下,已经抹掉部分所占bitmap的比例,onMeasure()方法中有一个int[]类型的数组 bmPixels ,这个时候我们就要利用这个数组来得到这个比例。

在onTouchEvent()方法的case MotionEvent.ACTION_UP加上一些代码:

@Override
  public boolean onTouchEvent(MotionEvent event) {
    int currX = (int) event.getX();
    int currY = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        //按下时,设置线条的起始点准备绘制
        path.moveto(currX,currY);
        //通过回调,实时把bitmap显示出去
        listener.bitmapchangelistener(newBitmap);
        break;
      case MotionEvent.ACTION_UP:
        //抬起手指时,计算图片抹去了多少
        int nullPixel = 0;
        newBitmap.getPixels(bmPixels,height);
        for (int i = 0; i < bmPixels.length; i++) {
          //抹去部分的像素点在数组中就会表示为0,找出为0的个数
          if (bmPixels[i] == 0) {
            nullPixeL++;
          }
        }
        //计算抹去部分所占的百分比
        listener.showBitmapClear((float) nullPixel / (float) bmPixels.length);
        break;
    }
    // 绘制线条,请求重绘整个控件
    canvas.drawPath(path,paint);
    //请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。
    invalidate();
    return true;
  }

有一句 newBitmap.getPixels(bmPixels,height);在getPixels方法详解中有解释,它的作用就是把newBitmap 中所有的像素点全部取出来,放到方法中的第一个参数bmPixels中。这个时候,我们再通过for循环遍历bmPixels数组,等于0的说明是没有颜色被抹掉的,统计他们的数量,计算他们所占的比例,就能算出抹掉的比例。同理我们也可以改变等于0这个判断条件,让他等于其他颜色,这样也就可以计算其他颜色所占比例。
写个回调接口,在代码中取出来就OK了。

//回调接口
  public interface bitmapListener {
    //实时的把绘制的bitmap显示在imageview 上
    void bitmapchangelistener(Bitmap bitmap);
    //显示抹掉比例
    void showBitmapClear(float clear);
  }

  public void addBitmapListener(bitmapListener bitmapListener) {
    this.listener = bitmapListener;
  }

有2个接口,一个实时的展示bitmap,一个展示抹去比例。

Android 自定义View之自定义属性

Android 自定义View之自定义属性

Android 自定义View之自定义属性

一:前言
image.png
1.什么是命名空间呢
android的命名空间和自定义命名空间
image.png

xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:前缀
android:名称,可以自定义
url:代表的就是空间,是个没有用的url,是统一资源标识符,相对于一个常量
2.配置文件attrs.xml
在res下的values文件加下创建一个attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomNumAnimView">
        <attr name="round_radius" format="dimension" />
        <attr name="round_color" format="color" />
        <attr name="text_color" format="color" />
        <attr name="text_size" format="dimension" />
    </declare-styleable>
</resources>

//格式解析
declare-styleable
name:属性集合名称
attr
name:属性名称
format:格式

共有11种格式
        1.reference(资源id)
    <ImageView android:background = "@drawable/图片ID"/>
    2.color
    <TextView android:textColor = "#00FF00" />
    3.boolean
    4.dimension(尺寸)(dp)
    5.float(浮点值)
    6.integer(整形值)
    7.string(字符串)
    8.fraction(百分比)
    9.enum(枚举值)
    <declare-styleable name="名称">
          <attr name="orientation">
      <enum name="horizontal" value="0" />
      <enum name="vertical" value="1" />
       </attr>
    </declare-styleable>
    10.flag(位或运算)
    注意:位运算类型的属性在使用的过程中可以使用多个值
    11.混合属性(使用|分开多个属性)

3.获取属性值

  public CustomNumAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray array=context.obtainStyledAttributes(attrs,R.styleable.CustomNumAnimView,defStyleAttr,0);
     int roundColor=array.getColor(R.styleable.CustomNumAnimView_round_color, ContextCompat.getColor(context,R.color.purple_200));
    float roundRadius=array.getDimension(R.styleable.CustomNumAnimView_round_radius,50);
     int  textColor=array.getColor(R.styleable.CustomNumAnimView_text_color, Color.WHITE);
    float  textSize=array.getDimension(R.styleable.CustomNumAnimView_text_size,30);
        array.recycle();
    }

二:自定义View使用自定义属性

public class CustomNumAnimView extends View {

    private int roundColor;    //圆的颜色
    private int textColor;    //数字的颜色
    private float textSize;    //数字字体大小
    private float roundRadius;    //圆的半径

    private Paint mPaint;     //画笔
    private Rect textRect;    //包裹数字的矩形

    private boolean isFirstInit = false;   //是否是第一次初始化

    private CustomPoint leftPoint;    //左边的数字的实时点
    private String leftNum = "9";
    private ValueAnimator leftAnim;   //左边数字动画
    private boolean isLeftNumInvalidate = false;  //左边数字是否重绘界面

    private CustomPoint middlePoint;   //中间的数字的实时点
    private String middleNum = "9";
    private ValueAnimator middleAnim;   //中间数字动画
    private boolean isMiddleNumInvalidate = false;    //中间数字是否重绘界面

    private CustomPoint rightPoint;    //右边的数字的实时点
    private String rightNum = "9";
    private ValueAnimator rightAnim;   //右边数字动画
    private boolean isRightNumInvalidate = false;    //右边数字是否重绘界面


    public CustomNumAnimView(Context context) {
        this(context, null);
    }

    public CustomNumAnimView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomNumAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomNumAnimView, defStyleAttr, 0);
        roundColor = array.getColor(R.styleable.CustomNumAnimView_round_color, ContextCompat.getColor(context, R.color.purple_200));
        roundRadius = array.getDimension(R.styleable.CustomNumAnimView_round_radius, 50);
        textColor = array.getColor(R.styleable.CustomNumAnimView_text_color, Color.WHITE);
        textSize = array.getDimension(R.styleable.CustomNumAnimView_text_size, 30);
        array.recycle();
        //创建画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//抗锯齿标志
        mPaint.setTextSize(textSize);//画笔设置文本大小
        textRect = new Rect();
        //得到数字矩形的宽高,以用来画数字的时候纠正数字的位置
        mPaint.getTextBounds(middleNum, 0, middleNum.length(), textRect);
    }

    /**
     * 测量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int size;
        int mode;
        int width;
        int height;
        size = MeasureSpec.getSize(widthMeasureSpec);
        mode = MeasureSpec.getMode(widthMeasureSpec);
        if (mode == MeasureSpec.EXACTLY) {    //确定的值或者MATCH_PARENT
            width = size;
        } else {    //表示WARP_CONTENT
            width = (int) (2 * roundRadius);
        }

        mode = MeasureSpec.getMode(heightMeasureSpec);
        size = MeasureSpec.getSize(heightMeasureSpec);
        if (mode == MeasureSpec.EXACTLY) {    //确定的值或者MATCH_PARENT
            height = size;
        } else {    //表示WARP_CONTENT
            height = (int) (2 * roundRadius);
        }
        setMeasuredDimension(width, height);
    }

    /**
     * 重写onDraw方法
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!isFirstInit) {
            //是
            //初始化三串数字
            leftPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
            middlePoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
            rightPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
            drawText(canvas);
            startAnimation();   //开始动画
            isFirstInit = true;

        } else {
            drawText(canvas);
        }
    }
    private boolean isAnimStart(ValueAnimator anim) {
        return !anim.isStarted();
    }
    public void startAnim() {
        if (isAnimStart(leftAnim)) {
            leftAnim.start();
        }
        if (isAnimStart(middleAnim)) {
            middleAnim.start();
        }
        if (isAnimStart(rightAnim)) {
            rightAnim.start();
        }
    }
    /**
     * 在onDestroy方法中调用
     */
    public void stopAnim() {
        leftAnim.end();
        middleAnim.end();
        rightAnim.end();
        leftAnim = null;
        middleAnim = null;
        rightAnim = null;
    }

    /**
     * 画数字
     */
    private void drawText(Canvas canvas) {
        //画圆
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setColor(roundColor);
        canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, roundRadius, mPaint);
        //写数字
        mPaint.setColor(textColor);
        mPaint.setTextSize(textSize);
        if (isLeftNumInvalidate) {
            canvas.drawText(leftNum, leftPoint.getX(), leftPoint.getY(), mPaint);
            isLeftNumInvalidate = false;
        }
        if (isMiddleNumInvalidate) {
            canvas.drawText(middleNum, middlePoint.getX(), middlePoint.getY(), mPaint);
            isMiddleNumInvalidate = false;
        }
        if (isRightNumInvalidate) {
            canvas.drawText(rightNum, rightPoint.getX(), rightPoint.getY(), mPaint);
            isRightNumInvalidate = false;
        }

    }

    /**
     * 开始动画
     */
    private void startAnimation() {
        startLeft();
        startMiddle();
        startRight();

    }

    private void startRight() {
        final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
        final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 + roundRadius * (Math.sqrt(3) / 2) + textRect.height() / 2));
        rightAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
        rightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                rightPoint = (CustomPoint) animation.getAnimatedValue();
                isRightNumInvalidate = true;
                invalidate();
            }
        });
        rightAnim.addListener(new CustomAnimListener() {
            @Override
            public void onAnimationRepeat(Animator animation) {
                rightNum = getRandom();
            }
        });
        rightAnim.setStartDelay(150);
        rightAnim.setDuration(300);
        rightAnim.setRepeatCount(ValueAnimator.INFINITE);

    }

    private void startMiddle() {
        //初始化中间数字的开始点的位置
        final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
        //初始化中间数字的结束点的位置
        final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 + roundRadius + textRect.height() / 2);
        middleAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
        //监听从起始点到终点过程中点的变化,并获取点然后重新绘制界面
        middleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                middlePoint = (CustomPoint) animation.getAnimatedValue();
                isMiddleNumInvalidate = true;
                invalidate();
            }
        });
        middleAnim.addListener(new CustomAnimListener() {
            @Override
            public void onAnimationRepeat(Animator animation) {
                middleNum = getRandom();
            }
        });
        middleAnim.setDuration(300);
        middleAnim.setRepeatCount(ValueAnimator.INFINITE);

    }

    private void startLeft() {
        //属性动画
        final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
        final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 + roundRadius * (Math.sqrt(3) / 2) + textRect.height() / 2));
        leftAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
        leftAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                leftPoint = (CustomPoint) animation.getAnimatedValue();
                isLeftNumInvalidate = true;
                invalidate();
            }
        });
        leftAnim.addListener(new CustomAnimListener() {
            @Override
            public void onAnimationRepeat(Animator animation) {
                middleNum = getRandom();
            }
        });
        leftAnim.setStartDelay(100);
        leftAnim.setDuration(300);
        leftAnim.setRepeatCount(ValueAnimator.INFINITE);
    }


    /**
     * 获取0-9之间的随机数
     *
     * @return
     */
    private String getRandom() {
        int random = (int) (Math.random() * 9);
        return String.valueOf(random);
    }
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:lsp="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.ruan.mygitignore.CustomNumAnimView
        android:id="@+id/custom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        lsp:round_radius="50dp"
        lsp:text_size="16dp"/>
</LinearLayout>

这是一个自定view的使用的自定义属性

结尾:每一个小小的进步,都是日后的财富

我们今天的关于Android 自定义View之随机数验证码仿写鸿洋,写给即将正在找工作的Android攻城狮的分享已经告一段落,感谢您的关注,如果您想了解更多关于Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解、Android 自定义View之倒计时实例代码、Android 自定义view之画图板实现方法、Android 自定义View之自定义属性的相关信息,请在本站查询。

本文标签: