【Android】自定义TextView

实现效果:点击文字控件,TextView的颜色会从左边或者右边开始滚动改变,从绿色逐步变为红色,
在这里插入图片描述

一、在开始之前希望你可以做一下以下工作,体验一下实际运行的效果:
1、打开AndroidStudio,新建一个工程
2、在main/res/values下新建一个attrs.xml,然后复制以下内容

    <?xml version="1.0" encoding="utf-8"?>
    <!--该文件是定义属性名和格式的地方-->
    <resources>
        <declare-styleable name="MyTextView">
            <!--format是该属性的取值类型-->
            <attr name="cmChangeTextColor" format="color"/>
            <attr name="cmOriginTextColor" format="color"/>
        </declare-styleable>
    </resources>

3、在MainActivity.java同级目录下,新建一个MyTextView.java,然后复制以下内容

    package com.你自己的包名,跟MainActivity一样即可;
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.Rect;
    import android.util.AttributeSet;
    import android.util.Log;
    
    public class MyTextView extends android.support.v7.widget.AppCompatTextView {
    
        private static final String TAG = "MyTextView";
    
        Paint OriginPaint,ChangePaint;
    
        //当前进度
        private float cmCurrentProgress;
    
        //默认朝向
        private Directory cmCurrentDirectory = Directory.LEFT_TO_RIGHT;
        public enum Directory{
            LEFT_TO_RIGHT,RIGHT_TO_LEFT
        }
    
        public MyTextView(Context context) {
            super(context);
        }
    
        //xml中构建的attrs使用该构造函数
        public MyTextView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context,attrs);
        }
    
        //该构造函数用于代码中自定义控件时指定style
        public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context,attrs);
    
        }
    
        private void init(Context context,AttributeSet attributeSet){
            Log.d(TAG, "init: 获取自定义属性");//获取自定义属性
    
            //获取颜色
            TypedArray typedArray=context.obtainStyledAttributes(attributeSet,R.styleable.MyTextView);
            int defValue=getTextColors().getDefaultColor();
            int OriginColor=typedArray.getColor(R.styleable.MyTextView_cmOriginTextColor,defValue);
            int ChangeColor=typedArray.getColor(R.styleable.MyTextView_cmChangeTextColor,defValue);
            typedArray.recycle();
    
            //创建画笔
            OriginPaint=getTextPaintByColor(OriginColor);
            ChangePaint=getTextPaintByColor(ChangeColor);
        }
    
        Paint getTextPaintByColor(int textColor){
            Log.d(TAG, "getTextPaintByColor: 获取画笔");
            //创建画笔
            Paint paint = new Paint();
            //设置画笔颜色
            paint.setColor(textColor);
            //设置抗锯齿
            paint.setAntiAlias(true);
            //设置防抖动
            paint.setDither(true);
            //设置字体大小
            paint.setTextSize(getTextSize());
    
            return paint;
        }
    
        //在这个方法中执行绘制操作,传入画笔和画布
        @Override
        protected void onDraw(Canvas canvas) {
            //根据当前进度,获取当前中间值
            int middle = (int) (cmCurrentProgress * getWidth());
    
            //根据朝向,绘制TextView
            if(Directory.LEFT_TO_RIGHT == cmCurrentDirectory){
                //当前朝向为  从左到右
                drawMyText(canvas,ChangePaint,0,middle);
                drawMyText(canvas,OriginPaint,middle,getWidth());
            }else{
                //当前朝向  从右到左
                drawMyText(canvas,ChangePaint,getWidth() - middle,getWidth());
                drawMyText(canvas,OriginPaint,0,getWidth()-middle);
            }
        }
    
        /*
         * synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),
         * 运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),
         * 有的话要等正在使用synchronized方法的线程B(或者C 、D)
         * 运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。*/
        public synchronized  void setDirectory(Directory directory){
            this.cmCurrentDirectory = directory;
        }
        public synchronized  void setCurrentProgress(float currentProgress){
            this.cmCurrentProgress = currentProgress;
            //刷新界面
            invalidate();
        }
    
        private void drawMyText(Canvas canvas, Paint textPaint, int start, int end) {
    
            Log.d(TAG, "drawMyText: 绘制文字");
    
            //保存画布状态
            canvas.save();
    
            //设置绘制区域
            Rect rect = new Rect(start,0,end,getHeight());
            //裁剪
            canvas.clipRect(rect);
    
            //获取控件此时的文字
            String text = getText().toString();
    
            Rect bounds = new Rect();
    
            //测量text的宽和高
            textPaint.getTextBounds(text,0,text.length(),bounds);
    
            //获取字体的宽度
            int x = getWidth()/2 - bounds.width()/2;
    
            //获取基线
            Paint.FontMetricsInt fontMetricsInt = textPaint.getFontMetricsInt();
            int dy = (fontMetricsInt.bottom - fontMetricsInt.top)/2 - fontMetricsInt.bottom;
            int baseLine = getHeight()/2 + dy;
    
            //https://blog.csdn.net/qqqq245425070/article/details/79027979
            //这一步执行绘制,给出文字的内容和位置,就会按照Paint的要求去绘制
            canvas.drawText(text,x,baseLine,textPaint);
    
            //释放画布状态,既恢复Canvas旋转,缩放等之后的状态。
            canvas.restore();
        }
    }

4、打开activity_main.xml,复制以下内容

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        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="match_parent"
        tools:context=".MainActivity">
    
        <你自己的包名.MyTextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/app_name"
            android:clickable="true"
            android:focusable="true"
            android:textSize="24sp"
            android:id="@+id/id_textView"
            app:cmChangeTextColor="@color/colorAccent"
            app:cmOriginTextColor="@color/colorPrimary"
            />
    
    
    </LinearLayout>

5、打开MainActivity.java,复制以下内容

    package 你自己的包名,不需要修改;
    
    import android.animation.ObjectAnimator;
    import android.animation.ValueAnimator;
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.view.View;
    import android.view.animation.DecelerateInterpolator;
    
    public class MainActivity extends AppCompatActivity {
    
        int i=0;//用于点击事件的状态切换,设置不同的方向
        MyTextView myTextView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            myTextView=findViewById(R.id.id_textView);
            myTextView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                    /*
        在Android动画中,总共有两种类型的动画View Animation(视图动画)和Property Animator(属性动画);
        View Animation包括Tween Animation(补间动画)和Frame Animation(逐帧动画);
        Property Animator包括ValueAnimator和ObjectAnimation;
    
    */
    //                创建ValueAnimator实例,对象区间在浮点0~1之间
                    ValueAnimator valueAnimator= ObjectAnimator.ofFloat(0,1);
                    //设置动画时长
                    valueAnimator.setDuration(5000);
    
                    //设置插值器,可以设定动画的移动效果,速度,是否回弹等
                    //参考https://blog.csdn.net/qq_30889373/article/details/78881140
                    valueAnimator.setInterpolator(new DecelerateInterpolator());
                    //valueAnimator.setInterpolator(new OvershootInterpolator());
                    if (i==0){
                        myTextView.setDirectory(MyTextView.Directory.LEFT_TO_RIGHT);
                        i=1;
                    }else {
                        myTextView.setDirectory(MyTextView.Directory.RIGHT_TO_LEFT);
                        i=0;
                    }
    
                    //设置监听,回传的结果为当前的状态数值,0~1,然后设置当前进度
                    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            float animatedValue= (float) animation.getAnimatedValue();
                            myTextView.setCurrentProgress(animatedValue);
                        }
                    });
                    valueAnimator.start();
                }
            });
        }
    }

6、运行
如果没有成功运行,检查activity_main.xml中包名是否修改为你的包名,各个新建的文件中有没有标红的地方,有的话,着手修改。

在Android中,自定义控件算是UI中比较复杂的知识(对新手来说),一般学习Android UI部分,遇到的第一个坎可能是ListView,因为这个控件需要比较多的代码去实现,而且涉及到Adapter,涉及到子项布局的设计,每行数据的存取值,还有一些优化写法等等;然后可能会遇到一些需要自定义的控件,实现个性化的展示,这里需要涉及attrs、Paint、Canvas等等,为了好理解,我觉得有必要从后面往前讲起。

二、一直跟踪代码的流转,会停留在这里,

    canvas.drawText(text,x,baseLine,textPaint);

在我们自己写的代码中,是这一步执行了绘制的操作,你可以用Ctrl+F查看这个函数所在的位置。
这个方法所做的操作是,传入String字符串text,并告知它的位置(x,baseLine),然后按照textPaint的要求去绘制。

text的数据来源是用getText().toString()方法,直接获取这个控件的文字“MyTextView”。
x和baseLine,是与我们要绘制的文字的位置,这两个值都是int,如果你尝试修改这个值,将会发生有趣的变化,我们尝试在baseLine后面+30看会怎么样

    canvas.drawText(text,x,baseLine+30,textPaint);

再次运行:
在这里插入图片描述
这里可以看到,我们改了位置,所以绘制的MyTextView字符整体下降了30,但是为什么显示不全呢?因为这个控件的高度是wrap_content,所以控件的高度只有一个TextView的高度,而它绘制的区域却在这个高度以外(baseLine是基线位置,关于这个可以参考https://blog.csdn.net/qqqq245425070/article/details/79027979,说的通俗易懂),当你在activity_main.xml中修改TextView的高度时,它就能全部显示出来了。

现在我们聚焦到最后一个变量textPaint,就是它导致我们的TextView变得与众不同,我们称它为画笔,而Canvas我们称为画布,刚刚我们说的那个函数,相当于笔和布都准备好了,然后你来画。

现在我们来观察准备画笔的操作。
第一步把画笔拿出来,也就是新建一个paint对象,

    Paint paint = new Paint();

请按Ctrl+F查看这一句所在的位置。

然后准备颜料或笔头的一些材质什么的,重点关注一下颜色是怎么传进来的,我们使用

    paint.setColor(textColor);

这个方法设置画笔颜色,而textColor是一个int类型,传入了OriginColor和ChangeColor
两个值,我们知道ChangeColor这个值是红色,而这个值是怎么得到的呢?通过

    int ChangeColor=typedArray.getColor(R.styleable.MyTextView_cmChangeTextColor,defValue);

方法获取,注意MyTextView_cmChangeTextColor这个变量是来自attrs.xml中名为MyTextView的declare-styleable标签内的cmChangeTextColor,而这个cmChangeTextColor又被activity_main.xml中引用
在这里插入图片描述
可以看到上面的cmChangeTextColor取值为@color/colorAccent,而这个colorAccent正是红色,它在color中已经定义好了
在这里插入图片描述
其实我们也可以自己在activity_main.xml中随意更改一个颜色,看一下效果如何
在这里插入图片描述
运行
在这里插入图片描述
可以看到这时候颜色已经变为蓝色(注意activity_main.xml中的layout_height已经被我改100dp,所以可以全部显示)。

OK现在我们也明白了颜色的来源,那么我们来讲详细的画画过程。

三、一开始我们说绘制的方法是
canvas.drawText(text,x,baseLine,textPaint);
实际上,点击一下控件,就会触发很多次这个方法,实际上监听的执行函数在onDraw这里,你可以在这里加一句日志看一下到底点一下执行了多少次这个方法

Log.d(TAG, "onDraw: middle=“+middle+" cmCurrentProgress="+cmCurrentProgress+" getWidth="+getWidth());

在这里插入图片描述
那么打印出来的结果是:
在这里插入图片描述



在这里插入图片描述
可以看出来,middle这个值是从1到1080的变化的,不管是从左往右还是从右往左。
每执行一次,红色的内容就涂一点点,一直涂到满为止。

注意从右向左的drawMyText方法,执行的是getWidth() - middle,也就是当middle为0的时候,从1080的位置开始绘制,然后逐渐减下来。
在这里插入图片描述
在MainActivity中,只要设置ValueAnimator动画,做一些简单的初始化即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值