Android自定义View
自绘控件的意思就是,这个View上所展现的内容全部都是我们自己绘制出来的。绘制的代码是写在onDraw()方法中的。
自定义一个计数器View,这个View可以响应用户的点击事件,并自动记录一共点击了多少次。新建一个CounterView继承自View,代码如下所示:
publicclass CountView extends View implements OnClickListener{private Paint mPaint;
private Rect mRect;
privateintmCount;
public CountView(Context context) {
super(context);
}
public CountView(Context context,AttributeSet attributeSet) {
super(context,attributeSet);
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);//创建画笔绘制抗锯齿
mRect=new Rect();
setOnClickListener(this);
}
@Override
protectedvoid onDraw(Canvas canvas) {
super.onDraw(canvas);
//将画笔设置成蓝色 绘制CountView的区域
mPaint.setColor(Color.BLUE);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
//设置更改画笔的颜色 绘制CountView区域上的计数的文字
mPaint.setColor(Color.RED);
mPaint.setTextSize(30);
String text=String.valueOf(mCount);//将计数的整数转换成字符串
//通过getTextBounds方法得到绘制文字的宽度和高度
mPaint.getTextBounds(text, 0, text.length(), mRect);
//获取区域中文字的宽度和高度
float textWidth=mRect.width();
float textHeigth=mRect.height();
canvas.drawText(text,getWidth()/2-textWidth/2, getHeight()/2+textHeigth/2, mPaint);
}
@Override
publicvoid onClick(View v) {
mCount++;
invalidate();//重绘数据
}
}
首先在CounterView的构造函数中初始化了一些数据,并给这个View的本身注册了点击事件,这样当CounterView被点击的时候,onClick()方法就会得到调用。而onClick()方法中的逻辑就更加简单了,只是对mCount这个计数器加1,然后调用invalidate()方法。调用invalidate()方法会导致视图进行重绘,因此onDraw()方法在稍后就将会得到调用。
CounterView是一个自绘视图,那么最主要的逻辑当然就是写在onDraw()方法里的了。这里首先是将Paint画笔设置为蓝色,然后调用Canvas的drawRect()方法绘制了一个矩形,这个矩形也就可以当作是CounterView的背景图吧。接着将画笔设置为黄色,准备在背景上面绘制当前的计数,注意这里先是调用了getTextBounds()方法来获取到文字的宽度和高度,然后调用了drawText()方法去进行绘制就可以了。
这样,一个自定义的View就已经完成了,并且目前这个CounterView是具备自动计数功能的。那么剩下的问题就是如何让这个View在界面上显示出来了,其实这也非常简单,我们只需要像使用普通的控件一样来使用CounterView就可以了。比如在布局文件中加入如下代码:
<com.qianfeng.customview3.CountView
android:layout_width="100dp"
android:layout_height="100dp"
/>
可以看到,这里我们将CounterView放入了一个RelativeLayout中,然后可以像使用普通控件来给CounterView指定各种属性,比如通过layout_width和layout_height来指定CounterView的宽高,通过android:layout_centerInParent来指定它在布局里居中显示。需要注意,自定义的View在使用的时候一定要写出完整的包名,不然系统将无法找到这个View。
好了,就是这么简单,接下来我们可以运行一下程序,并不停地点击CounterView,效果如下图所示。
怎么样?是不是感觉自定义View也并不是什么高级的技术,简单几行代码就可以实现了。当然了,这个CounterView功能非常简陋,只有一个计数功能,因此只需几行代码就足够了,当你需要绘制比较复杂的View时,还是需要很多技巧的。
组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。
标题栏就是个很常见的组合控件,很多界面的头部都会放置一个标题栏,标题栏上会有个返回按钮和标题,点击按钮后就可以返回到上一个界面。那么下面我们就来尝试去实现这样一个标题栏控件。
新建一个title.xml布局文件,代码如下所示:
<?xmlversion="1.0"encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#f47920">
<TextView
android:id="@+id/textview_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:text="This is Title"
android:textColor="#fff"
android:textSize="25sp"
/>
<Button
android:id="@+id/button_left"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:background="@drawable/btn_skin_down_bg"
android:text="back"
android:textColor="#fff"
android:textSize="20sp"
/>
</RelativeLayout>
publicclass TitleView extends FrameLayout {
private Button leftButton;
private TextView titleText;
public TitleView(Context context) {
super(context);
}
public TitleView(Context context,AttributeSet attributeSet) {
super(context,attributeSet);
//将自定义布局xml组合控件文件转换成view 注意:需要指定viewpgroup
LayoutInflater.from(context).inflate(R.layout.activity_title, this);
titleText=(TextView) findViewById(R.id.textview_title);
leftButton=(Button) findViewById(R.id.button_left);
leftButton.setOnClickListener(new OnClickListener() {
@Override
publicvoid onClick(View v) {
((Activity)getContext()).finish();
}
});
}
//提供方便使用的设置标题的方法
publicvoid setTextTitle(String textTitle){
titleText.setText(textTitle);
}
//提供方便使用的设置button文本的方法
publicvoid setTextButtonTitle(String buttonTitle){
leftButton.setText(buttonTitle);
}
//设置按钮的单击回调方法
publicvoid setButtonClickListener(OnClickListener listener){
leftButton.setOnClickListener(listener);
}
}
TitleView中的代码非常简单,在TitleView的构建方法中,我们调用了LayoutInflater的inflate()方法来加载刚刚定义的title.xml布局。
调用findViewById()方法获取到了返回按钮的实例,然后在它的onClick事件中调用finish()方法来关闭当前的Activity,也就相当于实现返回功能了。
另外,为了让TitleView有更强地扩展性,我们还提供了setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法,分别用于设置标题栏上的文字、返回按钮上的文字、以及返回按钮的点击事件。
自定义的标题栏就完成了,那么下面又到了如何引用这个自定义View的部分,其实方法基本都是相同的,在布局文件中添加如下代码:
<com.qianfeng.customview4.TitleView
android:id="@+id/titleview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
这样就成功将一个标题栏控件引入到布局文件中了,运行一下程序,效果如下图所示:
点击一下Back按钮,就可以关闭当前的Activity了。如果你想要修改标题栏上显示的内容,或者返回按钮的默认事件,只需要在Activity中通过findViewById()方法得到TitleView的实例,然后调用setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法进行设置就OK了。
继承控件只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能。
相关案例代码如下:
自定义EditText代码如下:
/*** 继承edittext需要完成的功能
* 1.默认情况edittext右边显示的默认图片 当文本发生改变时切换成其它图片
* 2.当单击其它图片时删除edittext中的输入内容
*/
publicclass MyEditText extends EditText {
private Drawable defaultDrawable,choseDrawable;//表示默认的图片对象与选中的图片对象
private Context mContext;
public MyEditText(Context context) {
super(context);
mContext=context;
init();
}
public MyEditText(Context context,AttributeSet attributeSet) {
super(context,attributeSet);
mContext=context;
init();
}
/*
* 初始化方法 加载图片 设置图片在edittext的右边
*/
publicvoid init(){
//加载drawable文件夹下的图片
defaultDrawable=mContext.getResources().getDrawable(R.drawable.btn_title_back_default_theme_1);
choseDrawable=mContext.getResources().getDrawable(R.drawable.btn_title_back_default_theme_2);
//文本改变监听 当文本改变时将图片切换成选中的图片
addTextChangedListener(new TextWatcher() {
@Override
publicvoid onTextChanged(CharSequence s, int start, int before, int count) {
setDrawable();
}
@Override
publicvoid beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
publicvoid afterTextChanged(Editable s) {
}
});
setDrawable();
}
/*
设置edittext中的图片
*/
publicvoid setDrawable(){
if(length()>1){//获取edittext中的文本长度 如果大于1显示选中的图片
setCompoundDrawablesWithIntrinsicBounds(null, null, choseDrawable,null);
}else{
//设置文字与图片摆放的位置 参数表示左上右下摆放的图片drawable对象
setCompoundDrawablesWithIntrinsicBounds(null, null, defaultDrawable, null);
}
}
//触摸的回调方法
@Override
publicboolean onTouchEvent(MotionEvent event) {
if(choseDrawable!=null && event.getAction()==MotionEvent.ACTION_UP){//获取触摸事件的动作 如果手指离开屏幕
// 获取触摸区域的x y的坐标
int eventX=(int) event.getRawX();
int eventY=(int) event.getRawY();
Rect rect=new Rect();
getGlobalVisibleRect(rect);
rect.left=rect.right-50;
if(rect.contains(eventX, eventY)){
//清空edittext的数据
setText("");
}
}
returnsuper.onTouchEvent(event);
}
自定义EditText XML使用如下:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<com.qianfeng.customview3.MyEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名"/>
<com.qianfeng.customview3.MyEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入密码"/>
</LinearLayout>
效果如图所示:
自定义记事本相关代码如下所示:
自定义EditText类代码如下:
/*** 实现类似记事本的样式,就像人们使用的写字的笔记本。<br/>
* 继承EditText实现绘制,利用 EditText 的某些方法可以计算出来总高度和每一行的高度,以及 文字BaseLine的尺寸,<br/>
* 这样就可以实现按照行高画线
*/
publicclass NoteEdit extends EditText {
/**
* 保存第一行的矩形尺寸,包含左右坐标
*/
private Rect lineBound;
/**
* 画线的样式
*/
private Paint linePaint;
/**
* 带有属性的构造方法,这个方法由layout加载View时调用
*
* @param context Context 上下文
* @param attrs 属性集合
*/
public NoteEdit(Context context, AttributeSet attrs) {
super(context, attrs);
lineBound = new Rect();
linePaint = new Paint();
int color = Color.BLACK;
// 获取定义的属性集合,注意,obtainStyledAttributes 第一个参数是传递过来的属性集合
// 第二个参数是在 attrs.xml 的 <declare-styleable name="NoteEdit"> 这个内容,这样再获取
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NoteEdit);
// 如果没有在 layout设定自定义样式,是找不到这个typedArray的,所以上面设置了一个 语句 int color = Color.BLACK;
if (typedArray != null) {
// 此时的 typedArray 存储的是 attrs.xml 中定义的属性数据集合
// 通过 R.styleable.NoteEdit_noteLineCode 即可实现获取指定的颜色属性 "noteLineColor"
color = typedArray.getColor(R.styleable.NoteEdit_noteLineColor, Color.BLACK);
// 属性获取之后,即可回收内容
typedArray.recycle();
}
// 设置好获取出来的线段颜色
linePaint.setColor(color);
}
/**
* 绘制方法<br/>
* 这个绘制方法需要调用super.onDraw(Canvas canvas) <br/>
* 因为用已有控件,所以直接调用父类的方法就可以绘制文字等原来的EditText内容
*
* @param canvas Canvas
*/
@Override
protectedvoid onDraw(Canvas canvas) {
// 依据基线绘制
// drawLineWithBaseline(canvas);
// 获取当前EditText的总高度,这个高度在 onDraw方法时,已经是测量过的尺寸
int height = getHeight();
// 一行的高度
int lineHeight = getLineHeight();
// 计算出当前总高度能够容纳多少行
int num = height / lineHeight;
// 获取一行的尺寸,这个尺寸是相对控件的,也就是获取控件内容区显示宽度
intbb = getLineBounds(0, lineBound);
int currentY = lineHeight;
// 按照行数来绘制
for (int i = 0; i < num; i++) {
// 每一个线段都是从前面行数累加出来一个 相对与控件的 y 坐标,也就是 currentBase的位置
currentY = i * lineHeight;
// 绘制一条水平线
canvas.drawLine(lineBound.left, currentY, lineBound.right, currentY, linePaint);
}
// 调用绘制原始的 EditText内容
super.onDraw(canvas);
}
自定义属性如下所示:res/values/attrs.xml
<resources>
<!-- 定义自定义属性集合 每一个 declare-styleable 可以包含多个属性字段 -->
<!-- 在代码获取样式的时候,先获取
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NoteEdit);
-->
<!-- 上面代码的含义是 获取属性集合的名称定义 -->
<declare-styleablename="NoteEdit">
<!-- 定义属性名称,这个就是用在 layout中的 -->
<!-- 代码获取这个属性应该这样获取:
color = typedArray.getColor(R.styleable.NoteEdit_noteLineColor, Color.BLACK);
-->
<attrname="noteLineColor"format="color"/>
</declare-styleable>
</resources>
layout布局文件代码:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:notepad="http://schemas.android.com/apk/res/com.qianfeng.mynote"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MainActivity"
/>
<com.qianfeng.mynote.NoteEdit
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:gravity="top"
android:textColor="#FFFF0000"
notepad:noteLineColor="#FF00FF00"
android:text="abcdefghijklmn"
/>
</LinearLayout>
效果如果所示: