我的第一篇csdn博客 -- 自定义View

学android已经小半年了,渣渣一枚,今天开始呢开始第一篇博客,一是分享自己的知识,二是记录自己的学习过程。

来张效果图吐舌头


注意如果你设置了click或者longclick事件时,你点击View时,会有按下的效果,像button一样。

本控件继承自TextView,所以具备TextView的所有属性,同时新增了一些自定义属性,所以首先看看自定义属性。

在value文件夹下增加attrs文件,添加以下代码

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TipView">
        <attr name="radius" format="dimension"/>
        <attr name="arrowHeight" format="dimension"/>
        <attr name="bgColor" format="color"/>
        <attr name="bgColor_pressed" format="color"/>
        <attr name="direction">
            <enum name="left" value="0"/>
            <enum name="top" value="1"/>
            <enum name="right" value="2"/>
            <enum name="bottom" value="3"/>
        </attr>
    </declare-styleable>

</resources>

首先declare-styleable 声明了你要定义的属性,name一般取类名,建议这么做,因为这样很容易区分。

下面的attr就是属性了,名字就是属性的名字啦,尴尬,format就是指属性值的类型,如下图所示,看名字应该知道什么类型吧!

下面解释各个属性的含义:

radius : tipview的圆角的半径。

arrowHeight :tipview箭头的高度。

bgColor,bgColor_pressed:背景(按下时)颜色

direction:定义了箭头的位置,是个枚举类型,分别代表上下左右的位置。

有了自定义属性,就可以在xml文件中使用(一个不使用不会出错,都有默认值安静),for example:

<com.wanli.lcc.learnproject.TipView
            android:id="@+id/tipView1"
            android:padding="10dp"
            app:bgColor="@color/purple"
            app:direction="right"
            app:bgColor_pressed="@color/purple_pressed"
            android:layout_marginTop="15dp"
            android:text="@string/english_02"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
属性前面的app代表命名空间,随便命名,但别忘了引入命名空间

xmlns:app="http://schemas.android.com/apk/res-auto"
然后看怎么在代码中使用这些属性啦。

首先是类的属性:

    

    //注意这里的应该和属性文件的枚举一一对应
    public static final int left = 0;
    public static final int top = 1;
    public static final int right = 2;
    public static final int bottom = 3;
    /**
     * 画笔
     */
    private Paint mPaint;

    /**
     * 默认背景色
     */
    private int bgColor = Color.parseColor("#88009900");
    /**
     *
     * 默认按下的背景
     */
    private int bgColor_pressed = Color.parseColor("#a9009900");
    /**
     * 当前要绘制的颜色
     */
    private int nowColor;

    /**
     * 气泡路径
     */
    private Path path;

    /**
     * 半径,默认30
     */
    private float radius = 30;
    /**
     * 箭头大小,默认30
     */
    private float arrowHeight = 30;


    private int paddingLeft,paddingTop,paddingRight,paddingBottom;

    /**
     * 默认方向,用户不设置时朝左
     */
    private int direction = left;

    /**
     * 视图是否无效
     */
    private boolean invalidate;
各个属性会在后面一一说明。

构造方法

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

    public TipView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TipView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        /**
         * 获取自定义参数
         */
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TipView,defStyleAttr,0);
        radius = ta.getDimension(R.styleable.TipView_radius,radius);
        arrowHeight = ta.getDimension(R.styleable.TipView_arrowHeight, arrowHeight);
        bgColor = ta.getColor(R.styleable.TipView_bgColor, bgColor);
        bgColor_pressed = ta.getColor(R.styleable.TipView_bgColor_pressed, bgColor_pressed);
        direction = ta.getInt(R.styleable.TipView_direction, direction);
        nowColor = bgColor;
        ta.recycle();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        init();
    }
下面一句就是获得我们声明的自定义属性,然后再获得各个属性的值,其中,你发现什么规律了么?
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TipView,defStyleAttr,0);
比如
radius = ta.getDimension(R.styleable.TipView_radius,radius);
中的TipView_radius下划线前面就是declare-styleable说明的名字,后面是具体attr,而这一切是安卓自动完成的,所以获得declare-styleable下的属性方法就是declare-styleable的name下划线加attr的name。

别忘了ta.recycle();,否则可能会影响下一次使用。


获得了具体属性,到了激动人心的时刻啦,开始绘制视图了。。。生气

大家都知道,安卓所有的视图控件什么的都继承View,最终调用系统回调View的onDraw方法呈现到屏幕上,所以,改变view样式就得重写onDraw方法。

分析一下TipView的原理:

首先绘制背景的矩形,就是一个圆角矩形了吐舌头,计算出箭头的位置,画出箭头,再调用父类的onDraw方法绘制字符串(你不用关心)。注意:其中咱们的绘制要放在父类绘制前面,不然咱们的绘制把字符串都遮住了,囧。。。

    @Override
    protected void onDraw(Canvas canvas) {
        if(path == null || invalidate) {
            if (direction == bottom) {
                path = new PathBuilder().builderBottomPath(canvas);
            } else if (direction == top) {
                path = new PathBuilder().builderTopPath(canvas);
            } else if (direction == right) {
                path = new PathBuilder().builderRightPath(canvas);
            } else {
                path = new PathBuilder().builderLeftPath(canvas);
            }
            if(invalidate)
            {
                invalidate = false;
            }
        }
        mPaint.setColor(nowColor);
        canvas.drawPath(path, mPaint);
        super.onDraw(canvas);
    }
根据方向获得一个path对象,设置颜色,绘制path,调用父类方法绘制,完成 大笑,教程结束。(等等,细节呢,你丫找死啊。。。。)
首先看看PathBuilder类,是一个内部类,里面四个方法,以绘制左边箭头为例:

        public Path builderBottomPath(Canvas canvas)
        {   
            Path path = new Path();
            RectF rectF = new RectF(canvas.getClipBounds());
            rectF.bottom -= arrowHeight;
            path.addRoundRect(rectF, radius, radius, Path.Direction.CW);
            float middleX = rectF.width() / 2;
            path.moveTo(middleX, getHeight());
            float arrowDx = arrowHeight * 0.7f;
            path.lineTo(middleX - arrowDx,rectF.bottom);
            path.lineTo(middleX + arrowDx,rectF.bottom);
            path.close();
            return path;
        }
根据传进来的画布canvas获得边界矩形,注意咱们的arrow的绘制区域是TipView的padding区域,意思就是,用户设置了padding后,我们再增加padding值,比如当箭头在左边时,我们设置paddingleft再加上arrowheight的大小,这多出来的padding值就来绘制箭头,而字符串也不会绘制到padding里,perfect!

       private void initPadding() {
        if(direction == bottom)
        {
            setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom + (int) arrowHeight);
        }
        else if (direction == top)
        {
            setPadding(paddingLeft, paddingTop + (int) arrowHeight, paddingRight, paddingBottom);
        }
        else if (direction == right)
        {
            setPadding(paddingLeft, paddingTop, paddingRight + (int) arrowHeight, paddingBottom);
        }
        else
        {
            setPadding(paddingLeft + (int) arrowHeight, paddingTop, paddingRight, paddingBottom);
        }
    }
注意canvas.getClipBounds()获取的矩形包括padding,rectF.bottom -= arrowHeight;就是让矩形缩小,只包括文字内容。然后绘制箭头,就在刚才腾出来的位置,具体位置大家拿笔试一下就知道位置了,当然你也可以绘制在自己喜欢的位置。这里就不演示了,一会儿会分享源码。
这样运行就能看见如图效果啦。下面添加按钮效果:

一个View有很多状态,按下,获得焦点等等,而当view的状态改变时会回调drawableStateChanged()方法,我们根据不同的状态绘制不同的背景就达到了按下的效果,button的实现原理也是这样。看代码:

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        /**
         * 当视图可以按的时候才判断状态,否则会出问题
         */
        if(isClickable() || isLongClickable()) {
            final int[] state = getDrawableState();
            boolean pressed = false;
            for (int i : state) {
                if (i == android.R.attr.state_pressed) {
                    pressed = true;
                    nowColor = bgColor_pressed;
                    invalidate();
                    break;
                }
            }
            if (!pressed) {
                nowColor = bgColor;
                invalidate();
            }
        }
    }
final int[] state = getDrawableState();获取当前的状态集合(不要奇怪,一个view一个时刻可能有很多状态),然后判断是否存在android.R.attr.state_pressed状态(现在只关心android.R.attr.state_pressed这一个状态),有的话就改变当前背景颜色nowColor,然后调用invalidate()重绘,没有的话也重绘恢复到默认状态。这样就能根据不同状态绘制不同颜色,而不需要多余的drawable文件。

还有注意到当用户动态设置Text时。字符串长度会变化,所以要重绘背景,bool invalidate 就是记录视图是否需要重绘,重写父类的setText方法:

@Override
    public void setText(CharSequence text, BufferType type) {
        /**
         * 当用户调用setText时大小可能变化,因此invalidate为true
         */
        invalidate = true;

        super.setText(text, type);
    }

 其中super.setText()会调用onDraw()方法,这样我们的背景也得到重绘啦。 

另外又增加了一个动态设置direcrion的方法,如下:

public void setDirection(int direction) {
        this.direction = direction;
        invalidate = true;
        initPadding();
        invalidate();
    }
记得重绘哦 微笑


好啦教程到这里就结束了,其实就是一个非常简单的textview,不过还是能学到很多知识的,比如自定义属性,不同状态等,这是我的第一篇博客,写的好渣尴尬,有什么问题欢迎大家提出来,一起进步大笑

另,因为就是一个小自定义view,不上传源码了,直接贴上来,还有别忘了把上面的属性文件加进来哦


全部代码:


package com.wanli.lcc.learnproject;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.widget.TextView;

/**
 * Created by lcc on 2015/8/25.
 */
public class TipView extends TextView{

    public static final int left = 0;
    public static final int top = 1;
    public static final int right = 2;
    public static final int bottom = 3;
    /**
     * 画笔
     */
    private Paint mPaint;

    /**
     * 默认背景色
     */
    private int bgColor = Color.parseColor("#88009900");
    /**
     *
     * 默认按下的背景
     */
    private int bgColor_pressed = Color.parseColor("#a9009900");
    /**
     * 当前要绘制的颜色
     */
    private int nowColor;

    /**
     * 气泡路径
     */
    private Path path;

    /**
     * 半径
     */
    private float radius = 30;
    /**
     * 箭头大小
     */
    private float arrowHeight = 30;


    private int paddingLeft,paddingTop,paddingRight,paddingBottom;

    /**
     * 默认方向
     */
    private int direction = left;

    /**
     * 视图是否无效
     */
    private boolean invalidate;

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

    public TipView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TipView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        /**
         * 获取自定义参数
         */
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TipView,defStyleAttr,0);
        radius = ta.getDimension(R.styleable.TipView_radius,radius);
        arrowHeight = ta.getDimension(R.styleable.TipView_arrowHeight, arrowHeight);
        bgColor = ta.getColor(R.styleable.TipView_bgColor, bgColor);
        bgColor_pressed = ta.getColor(R.styleable.TipView_bgColor_pressed, bgColor_pressed);
        direction = ta.getInt(R.styleable.TipView_direction, direction);
        nowColor = bgColor;
        ta.recycle();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        init();
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        /**
         * 当用户调用setText时大小可能变化,因此invalidate为true
         */
        invalidate = true;

        super.setText(text, type);
    }

    private void init() {

        paddingLeft = getPaddingLeft();
        paddingTop = getPaddingTop();
        paddingRight = getPaddingRight();
        paddingBottom = getPaddingBottom();
        initPadding();
    }
    /**
     * 根据方向确定预留空间,使用padding腾出画箭头的地方
     */
    private void initPadding() {
        if(direction == bottom)
        {
            setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom + (int) arrowHeight);
        }
        else if (direction == top)
        {
            setPadding(paddingLeft, paddingTop + (int) arrowHeight, paddingRight, paddingBottom);
        }
        else if (direction == right)
        {
            setPadding(paddingLeft, paddingTop, paddingRight + (int) arrowHeight, paddingBottom);
        }
        else
        {
            setPadding(paddingLeft + (int) arrowHeight, paddingTop, paddingRight, paddingBottom);
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        /**
         * 当视图可以按的时候才判断状态,否则会出问题
         */
        if(isClickable() || isLongClickable()) {
            final int[] state = getDrawableState();
            boolean pressed = false;
            for (int i : state) {
                if (i == android.R.attr.state_pressed) {
                    pressed = true;
                    nowColor = bgColor_pressed;
                    invalidate();
                    break;
                }
            }
            if (!pressed) {
                nowColor = bgColor;
                invalidate();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(path == null || invalidate) {
            if (direction == bottom) {
                path = new PathBuilder().builderBottomPath(canvas);
            } else if (direction == top) {
                path = new PathBuilder().builderTopPath(canvas);
            } else if (direction == right) {
                path = new PathBuilder().builderRightPath(canvas);
            } else {
                path = new PathBuilder().builderLeftPath(canvas);
            }
            if(invalidate)
            {
                invalidate = false;
            }
        }
        mPaint.setColor(nowColor);
        canvas.drawPath(path, mPaint);
        super.onDraw(canvas);
    }

    private class PathBuilder
    {
        public Path builderBottomPath(Canvas canvas)
        {
            Path path = new Path();
            RectF rectF = new RectF(canvas.getClipBounds());
            rectF.bottom -= arrowHeight;
            path.addRoundRect(rectF, radius, radius, Path.Direction.CW);
            float middleX = rectF.width() / 2;
            path.moveTo(middleX, getHeight());
            float arrowDx = arrowHeight * 0.7f;
            path.lineTo(middleX - arrowDx,rectF.bottom);
            path.lineTo(middleX + arrowDx,rectF.bottom);
            path.close();
            return path;
        }
        public Path builderTopPath(Canvas canvas)
        {
            Path path = new Path();
            RectF rectF = new RectF(canvas.getClipBounds());
            rectF.top += arrowHeight;
            path.addRoundRect(rectF, radius, radius, Path.Direction.CW);
            float middleX = rectF.width() / 2;
            path.moveTo(middleX, 0);
            float arrowDx = arrowHeight * 0.7f;
            path.lineTo(middleX - arrowDx,rectF.top);
            path.lineTo(middleX + arrowDx,rectF.top);
            path.close();
            return path;
        }
        public Path builderRightPath(Canvas canvas)
        {
            Path path = new Path();

            RectF rectF = new RectF(canvas.getClipBounds());
            rectF.right -= arrowHeight;
            path.addRoundRect(rectF, radius, radius, Path.Direction.CW);

            float arrowDy = arrowHeight * 0.7f;
            float middleY = radius + arrowDy;
            path.moveTo(getWidth(), middleY);

            path.lineTo(rectF.right, middleY - arrowDy);
            path.lineTo(rectF.right, middleY + arrowDy);
            path.close();
            return path;
        }
        public Path builderLeftPath(Canvas canvas)
        {
            Path path = new Path();

            RectF rectF = new RectF(canvas.getClipBounds());
            rectF.left += arrowHeight;
            path.addRoundRect(rectF, radius, radius, Path.Direction.CW);

            float arrowDy = arrowHeight * 0.7f;
            float middleY = radius + arrowDy;
            path.moveTo(0, middleY);

            path.lineTo(arrowHeight, middleY - arrowDy);
            path.lineTo(arrowHeight, middleY + arrowDy);
            path.close();
            return path;
        }

    }
    public void setDirection(int direction) {
        this.direction = direction;
        invalidate = true;
        initPadding();
        invalidate();
    }
}

最后附上演示用的apk文件

http://download.csdn.net/detail/lcc_luffy/9078385



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值