学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();
}
}
http://download.csdn.net/detail/lcc_luffy/9078385