继承已有View实现自定义View
通过对android原生控件的研究,可以发现android中的控件都是继承view类,如textView、ImageView等,通过重写相关的方法来实现新的效果,通过这个我们得到两点:
我们可以在已有控件的基础上,通过重写相关方法来实现我们的需求。
继承view类或viewgroup类,来创建我们所需要的控件。一般来讲,通过继承已有的控件,来自定义控件要简单一点。
1.开关
这其实是俩张图片
主要步骤:
1、自定义类MyToggleButton继承自View
2、重写onMeasure方法,指定控件大小。
3、重写onDraw方法,绘制控件内容。
4、重写onTouchEvent方法,对touch事件进行解析。
MyToggleButton
自定义属性
<resources
<!-- 声名属性集的名称 -->
<declare-styleable name="MyToggleBtn">
<!-- 声名一个属性 name是my_background 类型为 引用类型 引用资源ID -->
<attr name="my_background" format="reference" />
<!-- 声名一个属性 name是my_slide_btn 类型为 引用类型 引用资源ID -->
<attr name="my_slide_btn" format="reference" />
<!-- 声名一个属性 name是curr_state 类型为 boolean 类型-->
<attr name="curr_state" format="boolean" />
</declare-styleable>
</resources>
布局
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:heima="http://schemas.android.com/apk/res/com.itheima.togglebtn28"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.itheima.togglebtn28.MyToggleButton
android:id="@+id/my_toggle_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
heima:my_background="@drawable/switch_background"
heima:my_slide_btn="@drawable/slide_button"
heima:curr_state="false"
testAttrs="@drawable/ic_launcher"
/>
</RelativeLayout>
代码
public class MyToggleButton extends View implements OnClickListener{
/**
* 做为背景的图片
*/
private Bitmap backgroundBitmap;
/**
* 可以滑动的图片
*/
private Bitmap slideBtn;
private Paint paint;
/**
* 滑动按钮的左边届
*/
private float slideBtn_left;
/**
* 背景图的资源ID
*/
private int backgroundId;
/**
* 滑动图片的资源ID
*/
private int slideBtnId;
/**
* 在代码里面创建对象的时候,使用此构造方法
*/
public MyToggleButton(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
/**
* 在布局文件中声名的view,创建时由系统自动调用。
* @param context 上下文对象
* @param attrs 属性集
*/
public MyToggleButton(Context context, AttributeSet attrs) {
super(context, attrs);
//无命名空间测试
String testAttrs = attrs.getAttributeValue(null, "testAttrs");
System.out.println("testAttrs===:"+testAttrs);
//获得自定义的属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyToggleBtn);
int N = ta.getIndexCount();
for (int i = 0; i < N; i++) {
/*
* 获得某个属性的ID值
*/
int itemId = ta.getIndex(i);
switch (itemId) {
case R.styleable.MyToggleBtn_curr_state:
currState = ta.getBoolean(itemId, false);
break;
case R.styleable.MyToggleBtn_my_background:
backgroundId = ta.getResourceId(itemId, -1);
if(backgroundId == -1){
throw new RuntimeException("请设置背景图片");
}
backgroundBitmap = BitmapFactory.decodeResource(getResources(), backgroundId);
break;
case R.styleable.MyToggleBtn_my_slide_btn:
slideBtnId = ta.getResourceId(itemId, -1);
slideBtn = BitmapFactory.decodeResource(getResources(), slideBtnId);
break;
default:
break;
}
}
initView();
}
/**
* 初始化
*/
private void initView() {
//初始化图片
// backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
// slideBtn = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button);
//初始化 画笔
paint = new Paint();
paint.setAntiAlias(true); // 打开抗矩齿
//添加onclick事件监听
setOnClickListener(this);
flushState();
}
/*
* view 对象显示的屏幕上,有几个重要步骤:
* 1、构造方法 创建 对象。
* 2、测量view的大小。 onMeasure(int,int);
* 3、确定view的位置 ,view自身有一些建议权,决定权在 父view手中。 onLayout();
* 4、绘制 view 的内容 。 onDraw(Canvas)
*/
@Override
/**
* 测量尺寸时的回调方法
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 设置当前view的大小
* width :view的宽度
* height :view的高度 (单位:像素)
*/
setMeasuredDimension(backgroundBitmap.getWidth(),backgroundBitmap.getHeight());
}
//确定位置的时候调用此方法
//自定义view的时候,作用不大
// @Override
// protected void onLayout(boolean changed, int left, int top, int right,
// int bottom) {
// super.onLayout(changed, left, top, right, bottom);
// }
/**
* 当前开关的状态
* true 为开??
*/
private boolean currState = false;
@Override
/**
* 绘制当前view的内容
*/
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
// 绘制 背景
/*
* backgroundBitmap 要绘制的图片
* left 图片的左边届
* top 图片的上边届
* paint 绘制图片要使用的画笔
*/
canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
//绘制 可滑动的按钮
canvas.drawBitmap(slideBtn, slideBtn_left, 0, paint);
}
/**
* 判断是否发生拖动,
* 如果拖动了,就不再响应 onclick 事件
*
*/
private boolean isDrag = false;
@Override
/**
* onclick 事件在View.onTouchEvent 中被解析。
* 系统对onclick 事件的解析,过于简陋,只要有down 事件 up 事件,系统即认为 发生了click 事件
*
*/
public void onClick(View v) {
/*
* 如果没有拖动,才执行改变状态的动作
*/
if(!isDrag){
currState = !currState;
flushState();
}
}
/**
* down 事件时的x值
*/
private int firstX;
/**
* touch 事件的上一个x值
*/
private int lastX;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = lastX =(int) event.getX();
isDrag = false;
break;
case MotionEvent.ACTION_MOVE:
//判断是否发生拖动
if(Math.abs(event.getX()-firstX)>5){
isDrag = true;
}
//计算 手指在屏幕上移动的距离
int dis = (int) (event.getX() - lastX);
//将本次的位置 设置给lastX
lastX = (int) event.getX();
//根据手指移动的距离,改变slideBtn_left 的值
slideBtn_left = slideBtn_left+dis;
break;
case MotionEvent.ACTION_UP:
//在发生拖动的情况下,根据最后的位置,判断当前开关的状态
if (isDrag) {
int maxLeft = backgroundBitmap.getWidth() - slideBtn.getWidth(); // slideBtn
// 左边届最大值
/*
* 根据 slideBtn_left 判断,当前应是什么状态
*/
if (slideBtn_left > maxLeft / 2) { // 此时应为 打开的状态
currState = true;
} else {
currState = false;
}
flushState();
}
break;
}
flushView();
return true; //将事件消费掉
}
/**
* 刷新当前状态
*/
private void flushState() {
if(currState){
slideBtn_left = backgroundBitmap.getWidth()-slideBtn.getWidth();
}else{
slideBtn_left = 0;
}
flushView();
}
/**
* 刷新当前视力
*/
private void flushView() {
/*
* 对 slideBtn_left 的值进行判断 ,确保其在合理的位置 即 0<=slideBtn_left <= maxLeft
*
*/
int maxLeft = backgroundBitmap.getWidth()-slideBtn.getWidth(); // slideBtn 左边届最大值
//确保 slideBtn_left >= 0
slideBtn_left = (slideBtn_left>0)?slideBtn_left:0;
//确保 slideBtn_left <=maxLeft
slideBtn_left = (slideBtn_left<maxLeft)?slideBtn_left:maxLeft;
/*
* 刷新当前视图 导致 执行onDraw执行
*/
invalidate();
}
}
注意这个方法是系统的invalidate
为新控件添加自定义的属性
一、在attrs.xml文件中声明属性,如:
<declare-styleable name="MyToggleBtn"> // 声名属性集的名称,即这些属性是属于哪个控件的。
<attr name="current_state" format="boolean"/> // 声名属性 current_state 格式为 boolean 类型
<attr name="slide_button" format="reference"/> // 声名属性 slide_button 格式为 reference 类型
</declare-styleable>
所有的format类型,详见注1:
二、在布局文件中使用:在使用之前必须声名命名空间,xmlns:heima="http://schemas.android.com/apk/res/com.itheima.mytogglebtn"
说明:xmlns 是XML name space 的缩写;
heima 可为任意写符
http://schemas.android.com/apk/res/ 此为android固定格式;
com.itheima.mytogglebtn 此应用的包名,如manifest配置文件中一致。
布局文件:
<com.itheima.mytogglebtn.MyToggleButton
xmlns:heima="http://schemas.android.com/apk/res/com.itheima.mytogglebtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
heima:slide_button="@drawable/slide_button" />
三、在代码中对属性进行解析,主要代码:
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyToggleBtn); // 由attrs 获得 TypeArray
注1:
format 常用类型
reference 引用
color 颜色
boolean 布尔值
dimension 尺寸值
float 浮点值
integer 整型值
string 字符串
enum 布尔值
2.水波纹
模拟水波纹的效果:
1、自定义类继承View
2、重写onTouchEvent方法,down时,获得坐标点,做为圆环圆心。
3、发送handler信息,对数据进行修改,刷新页面。
4、重写onDraw方法,绘制一个圆环。
5、如果可以执行动画,重复上面第3步。
MyRring:只能点一个点开始执行动画,点另一个点上一个动画就停止了,开始这个
public class MyRring extends View{
/**
* 圆环圆心的X坐标
*/
private int cx;
/**
* 圆环圆心的Y坐标
*/
private int cy;
private Paint paint;
/**
* 圆环的半径
*/
private float radius;
/**
* 线条的厚度
*/
private float strokeWidth;
public MyRring(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
//初始化paint
paint = new Paint();
paint.setAntiAlias(true); // 抗矩齿
paint.setColor(Color.RED);
paint.setStyle(Style.STROKE); //刻画,画线条
paint.setStrokeWidth(strokeWidth); //设置条线的厚度
paint.setAlpha(255); //设置透明度 ,0--255 0代表完全透明
//
this.radius =0;
strokeWidth = 0;
}
@Override
/**
* 绘制我们的内容
*/
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 绘制圆环
*/
canvas.drawCircle(cx, cy, radius, paint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 点击,获得圆环的中心
cx = (int) event.getX();
cy = (int) event.getY();
//初始化画笔
initView();
handler.sendEmptyMessage(0);
break;
}
return true;
}
/*
* 刷新状态
*/
private void flushState() {
this.radius+=10;
this.strokeWidth = radius/4;
paint.setStrokeWidth(strokeWidth);
int nextAlpha = paint.getAlpha()-20;
if(nextAlpha<=20){
nextAlpha = 0;
}
paint.setAlpha(nextAlpha);
}
private Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
flushState();
// 刷新页面 执行onDraw()方法
invalidate();
if(paint.getAlpha() !=0){
handler.sendEmptyMessageDelayed(0, 100);
}
};
};
@Override
/**
* 大小的测量按系统的默认规则
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
MyRing2
public class MyRing extends View{
/**
* 圆心的X坐标
*/
private float cx;
/**
* 圆心的Y坐标
*/
private float cy;
/**
* 圆环半径
*/
private float radius = 0;
/**
* 默认画笔
*/
private Paint paint;
protected boolean isRunning = false;
public MyRing(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
radius = 0;
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(radius/4);
paint.setColor(Color.GREEN);
paint.setAlpha(255);
}
/**
* 执行动画
*/
private void startAnim() {
isRunning = true;
handler.sendEmptyMessageDelayed(0, 50);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// startAnim() ;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isRunning = false;
}
private Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
// 设置透明度
int alpha = paint.getAlpha();
if (alpha == 0) {
isRunning = false;
}
// alpha -= 10;
//
// if (alpha <= 10) {
// alpha = 0;
// }
alpha = Math.max(0, alpha-10);
paint.setAlpha(alpha);
Log.i("leo", "" + alpha);
// 设置半径
radius += 5;
paint.setStrokeWidth(radius / 3);
invalidate();
if (isRunning) {
handler.sendEmptyMessageDelayed(0, 50);
}
};
};
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// setMeasuredDimension(200, 200);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
if(changed){
cx = getWidth()/2;
cy = getHeight()/2;
}
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(cx, cy, radius, paint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if(event.getAction() == MotionEvent.ACTION_DOWN){
cx = event.getX();
cy = event.getY();
initView();
startAnim();
}
return true;
}
}
MyRingSimple:可以点俩个点
public class MyRingSimple extends View{
/**
* 圆心的X坐标
*/
private float cx;
/**
* 圆心的Y坐标
*/
private float cy;
/**
* 圆环半径
*/
private float radius = 0;
/**
* 默认画笔
*/
private Paint paint;
protected boolean isRunning = false;
private Paint lPaint;
public MyRingSimple(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
radius = 50;
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(radius/4);
paint.setColor(Color.GREEN);
paint.setAlpha(255);
lPaint = new Paint();
lPaint.setAntiAlias(true);
lPaint.setStyle(Style.STROKE);
lPaint.setColor(Color.GREEN);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(200, 200);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
if(changed){
cx = getWidth()/2;
cy = getHeight()/2;
}
}
@Override
protected void onDraw(Canvas canvas) {
for (int i = 10; i < getWidth(); i=i+20) {
canvas.drawLine(0, i, getWidth(), i, lPaint);
canvas.drawLine(i, 0, i,getHeight(), lPaint);
}
paint.setColor(Color.GREEN);
canvas.translate(-20, -20);
canvas.drawCircle(cx, cy, radius, paint);
}
}
MyRingWave:完整,可以触摸
/**
* 水波纹效果
* @author leo
*
*/
public class MyRingWave extends View{
/**
* 二个相临波浪中心点的最小距离
*/
private static final int DIS_SOLP = 13;
protected boolean isRunning = false;
private ArrayList<Wave> wList;
public MyRingWave(Context context, AttributeSet attrs) {
super(context, attrs);
wList = new ArrayList<MyRingWave.Wave>();
}
private Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
//刷新数据
flushData();
//刷新页面
invalidate();
//循环动画
if (isRunning) {
handler.sendEmptyMessageDelayed(0, 50);
}
};
};
@Override
protected void onDraw(Canvas canvas) {
for (int i = 0; i < wList.size(); i++) {
Wave wave = wList.get(i);
canvas.drawCircle(wave.cx, wave.cy, wave.r, wave.p);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX();
int y = (int) event.getY();
addPoint(x,y);
break;
default:
break;
}
return true;
}
/**
* 添加新的波浪中心点
* @param x
* @param y
*/
private void addPoint(int x, int y) {
if(wList.size() == 0){
addPoint2List(x,y);
/*
* 第一次启动动画
*/
isRunning = true;
handler.sendEmptyMessage(0);
}else{
Wave w = wList.get(wList.size()-1);
if(Math.abs(w.cx - x)>DIS_SOLP || Math.abs(w.cy-y)>DIS_SOLP){
addPoint2List(x,y);
}
};
}
/**
* 添加新的波浪
* @param x
* @param y
*/
private void addPoint2List(int x, int y) {
Wave w = new Wave();
w.cx = x;
w.cy=y;
Paint pa=new Paint();
pa.setColor(colors[(int)(Math.random()*4)]);
pa.setAntiAlias(true);
pa.setStyle(Style.STROKE);
w.p = pa;
wList.add(w);
}
private int [] colors = new int[]{Color.BLUE,Color.RED,Color.YELLOW,Color.GREEN};
/**
* 刷新数据
*/
private void flushData() {
for (int i = 0; i < wList.size(); i++) {
Wave w = wList.get(i);
//如果透明度为 0 从集合中删除
int alpha = w.p.getAlpha();
if(alpha == 0){
wList.remove(i); //删除i 以后,i的值应该再减1 否则会漏掉一个对象,不过,在此处影响不大,效果上看不出来。
continue;
}
alpha-=5;
if(alpha<5){
alpha =0;
}
//降低透明度
w.p.setAlpha(alpha);
//扩大半径
w.r = w.r+3;
//设置半径厚度
w.p.setStrokeWidth(w.r/3);
}
/*
* 如果集合被清空,就停止刷新动画
*/
if(wList.size() == 0){
isRunning = false;
}
}
/**
* 定义一个波浪
* @author leo
*/
private class Wave {
//圆心
int cx;
int cy;
//画笔
Paint p;
//半径
int r;
}
}