![](https://img.haomeiwen.com/i1282768/d37b661406bff2bb.gif)
![](https://img.haomeiwen.com/i1282768/e4c1bc0f9bf5f1ff.gif)
2017/01/18
上海
Write endless of the View
GitHub:https://github.com/ImmortalHalfWu/ViewSample/blob/master/app/src/main/java/com/viewsample/viewsample/views/ProgressButtonView.java
一,样式及事件分析
按钮共有三种显示样式:
1,初始化状态,包含内外两个圆,静态。
2,长按后,外圈拉伸,内圈收缩,动态。
3,外圈拉伸结束并且内圈收缩结束,绘制进度条,动态。
按钮共有六种事件驱动:
1,初始化,控件加载。
2,单击,或者说短按。
3,短按抬手。
4,长按。
5,长按抬手。
6,进度条加载结束。
事件与样式的关系:
1,初始化、短按、短按抬手,统一的初始化样式。
2,长按,外圈拉伸,内圈收缩,之后绘制进度条。
3,长按抬手、进度条加载结束,恢复为初始化样式。
事件回调:
1,短按,短按抬手时回调。
2,长按按下,判断为长按后立刻回调。
3,长按抬起时回调。
4,进度条结束时回调。
5,长按后,手指滑动时回调。
二,模块划分
1,StateMachine状态机 + 低配MVC + 回调接口
StateMachine状态机 : 描述所有状态,并记录当下所处状态。
View : 测量自身宽高,绘制图形,接收触屏事件。
Model : 记录数值,例如控件宽高,以及绘制图形时的数值。
Controller : 处理触屏事件,针对事件修改Model数据,并通知View重绘。
CallBackListener:回调接口,将控件的不同状态及事件传递给外部。
流程:
1,初始化StateMachine、Model、Controller。
2,获取Model数值绘制图形。
3,接受触屏事件。
4,修改状态。
5,Controller根据状态修改Model绘制数值。
6,通知View重绘。
7,如果外部传入了回调接口,则回调。
三,自上而下的抽象:
StateMachine,状态机。
/**
* 状态机,描述不同的状态
*/
public enum StateMachine{
/**
* 初始化状态
*/
STATE_INITIA,
/**
* 短按
*/
STATE_SHORT_CLICK,
/**
* 短按抬手
*/
STATE_SHORT_UP,
/**
* 长按中
*/
STATE_LONG_CLICKING,
/**
* 长按抬手
*/
STATE_LONG_UP,
/**
* 进度条加载结束
*/
STATE_PROGRESS_OVER,
}
CallBackListener,回调接口.
/**
* 事件回调接口
*/
public interface CallBackListener{
/**
* 短按,抬手时回调
*/
void shortClick();
/**
* 长按,判断为长按后回调
*/
void longClick();
/**
* 长按抬起,与{@link #progressOver()}只有一个会回调
*/
void longClickUp();
/**
* 进度条结束 ,与{@link #longClickUp()}只有一个会回调
*/
void progressOver();
/**
* 手指滑动,只有在长按中滑动才会调用
* @param event
*/
void move(MotionEvent event);
}
View , 重点在于触屏与图形绘制,基于责任划分的目的,View本身应尽量避免不同状态下或不同事件下数值的计算。
//计算宽高,注意到控件是有伸缩效果的,所以得到的宽高应该是拉伸后的宽高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//计算拉伸后的宽高
}
//绘制图形,并只负责绘制图形,图形的参数数值不在此处理
protected void onDraw(Canvas canvas) {
//外圆
//进度条
//内圆
}
//触屏监听
public boolean onTouchEvent(MotionEvent event) {
//根据事件修改View所处的状态
}
//销毁
protected void onDetachedFromWindow(){
//释放资源
}
Model,记录绘制数值。(为了避免又多又长的变量名所造成的不适,变量以大白话汉字表示)
"控件宽度"
"控件高度"
"初始化时内圆半径" //初始化时的内圆半径
"初始化时外圆半径" //初始化时的外圆半径
"长按状态内圆半径" //长按状态下内圆半径
"长按状态外圆半径" //长按状态下外圆半径
"绘制时使用的内圆半径" //大于等于最小内圆半径,小于等于最大内圆半径
"绘制时使用的外圆半径" //大于等于最小外圆半径,小于等于最大外圆半径
"进度条宽度"
"进度条弧度"
"进度条最长时间"
"外圆颜色"
"内圆颜色"
"进度条颜色"
Controller,控制器,根据状态的不同,对Model数值进行不同的计算,之后刷新界面。
使用线程作为Controller,缺点是为了避免浪费CPU,需要掌控好线程的等待与唤醒,并且有驳于MVC的设计,好处是没必要在重复绘制形成动画结束后反复new Thread()。
//使用线程作为Controller
public class PoorThread extends Thread{
public void run() {
//针对不同的状态,进行不同的数值计算,并刷新界面
}
//线程等待
public void waitPoorThread(){
if ("是否处于等待状态") return;
synchronized (PoorThread.this){
try {
"是否处于等待状态"= true;
//等待
PoorThread.this.wait();
} catch (InterruptedException e) {
"是否处于等待状态"= false;
e.printStackTrace();
}
}
}
//线程唤醒
public void noitfyPoorThread(){
if (!"是否处于等待状态") return;
synchronized (PoorThread.this){
//唤醒
PoorThread.this.notify();
"是否处于等待状态"= false;
}
}
}
四,自下而上的实现
1,View层,宽高计算、样式绘制、接收触摸事件及释放资源
宽高计算:
因为样式核心是圆,为了省去不必要的麻烦,所以设定宽高相等(并不是必须相等)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//screenWid 为屏幕宽度
//screenHei 为屏幕高度
int widMod = MeasureSpec.getMode(widthMeasureSpec);
int heiMod = MeasureSpec.getMode(heightMeasureSpec);
//指定宽
if (widMod == MeasureSpec.EXACTLY){
//获取指定宽
mWidth = MeasureSpec.getSize(widthMeasureSpec);
}
//指定最大宽度
else if (widMod == MeasureSpec.AT_MOST){
//如果屏幕屏幕宽度/3大于指定最大宽度,则取最大宽度
mWidth = MeasureSpec.getSize(widthMeasureSpec) < screenWid / 3 ? MeasureSpec.getSize(widthMeasureSpec) : screenWid / 3;
}
//未指定宽度
else if (widMod == MeasureSpec.UNSPECIFIED){
//则取屏幕宽度/3
mWidth = screenWid / 3;
}
//高度同上
if (heiMod == MeasureSpec.EXACTLY){
mHeight = MeasureSpec.getSize(heightMeasureSpec);
}else if (heiMod == MeasureSpec.AT_MOST){
mHeight = MeasureSpec.getSize(heightMeasureSpec) < screenHei / 3 ? MeasureSpec.getSize(heightMeasureSpec) : screenHei / 3;
}else if (heiMod == MeasureSpec.UNSPECIFIED){
mHeight = screenHei / 3;
}
//求出宽高后,需要加上内边距,之后宽高取小(避免横竖屏切换带来的麻烦),因为控件是正方形,变量mSize是为了使用方便
mSize = mHeight = mWidth = Math.min(mHeight + getPaddingTop() + getPaddingBottom(),mWidth + getPaddingLeft() + getPaddingRight());
setMeasuredDimension(mWidth,mHeight);
//计算出宽高后,计算Model中数据
initValue();
}
//初始化Model数据
private void initValue() {
//mSize = "控件宽高取小";
"初始化时外圆半径"= mSize/3;
"长按状态外圆半径"= mSize/2;
"初始化时内圆半径"= mSize/4;
"长按状态内圆半径" = mSize/6;
"绘制时使用的内圆半径"= "初始化时外圆半径";
"绘制时使用的外圆半径"= "初始化时内圆半径";
"进度条宽度"= mSize/25;
"进度条弧度"= 0;
}
样式绘制(代码里双引号中的汉字并不是字符串,而是指代某个变量):
View的绘制顺序遵循后来居上,先画的会被后画的遮盖。
//长按状态下同初始化状态一样内外圆还是存在的,只是多了进度条。
//对于进度条,简单的实现方式是先画实心弧,再在弧上画圆,
//圆的颜色为背景色,圆的半径小于弧的半径,
//这样会遮盖弧的内部,只留外部的一圈,达到弧线的效果。
//这样的实现方式无需考虑画笔的宽度,也省去了计算宽高时的麻烦。
//在这个控件中,由底部到顶部的绘制顺序是:
//1,外圆。 2,弧。 3,遮盖弧的圆(与外圆颜色相等) 4,内圆。
//而进度条是在外圆拉伸结束、内圆收缩结束后才出现,所以需要加判断
//1,外圆。
//if(外圆拉伸结束、内圆收缩结束) { 2,弧。 3,遮盖弧的圆(与外圆颜色相等) }
//4,内圆。
//画外圆
mPaint.setColor("外圆的颜色");
canvas.drawCircle(
canvas.getWidth() / 2, //绘制在控件正中
canvas.getHeight() /2,
"绘制时使用的外圆半径",
mPaint
);
//当前状态为长按中,并且进度条弧度大于0,才绘制进度条
if( "如果当前状态为长按中" && "进度条弧度" > 0 ){
//画弧
mPaint.setColor("进度条颜色");
canvas.drawArc(
//RectF 应为成员变量,在此简写
new RectF(canvas.getWidth()/2- "绘制时使用的外圆半径",
canvas.getHeight()/2- "绘制时使用的外圆半径" ,
canvas.getWidth()/2+ "绘制时使用的外圆半径" ,
canvas.getHeight()/2+ "绘制时使用的外圆半径" ),
-90.0f,
"进度条弧度",
true,
mPaint
);
//画遮盖弧线的圆
mPaint.setColor("外圆的颜色");
//画在正中心,半径为长按状态下外圆最大半径 - 进度条宽度,也就是遮盖的半径
canvas.drawCircle(
canvas.getWidth() / 2,
canvas.getHeight() /2,
"长按状态下外圆最大半径" - "进度条宽度",
mPaint
);
}
//画内圆
mPaint.setColor("内圆的颜色");
canvas.drawCircle(
canvas.getWidth() / 2, //绘制在控件正中
canvas.getHeight() /2,
"绘制时使用的内圆半径",
mPaint
);
触摸事件处理:
主要任务是根据手势修改控件所处的状态。
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()){
//手指按下
case MotionEvent.ACTION_DOWN:
//如果不是初始状态,则直接返回,因为动画需要时间,避免冲突
if("当前状态" != StateMachine.STATE_INITIA){
return false;
}
//将状态切换为短按
"当前状态" = StateMachine.STATE_SHORT_CLICK;
//调用线程,250毫秒后运行,如果状态还是短按而没有抬手,则将状态切换为长按。
//(此Runnable应为成员变量,在此简写)
postDelayed(new Runnable(){
@Override
public void run() {
//如果当前状态为短按
if ("当前状态"== StateMachine.STATE_SHORT_CLICK){
//切换为长按
"当前状态" = StateMachine.STATE_LONG_CLICKING;
//唤醒数据处理线程
if (mPoorThread!= null){
mPoorThread.noitfyPoorThread();
}
//如果回调接口不为空,回调接口
if (mCallBackListener != null){
mCallBackListener.longClick();
}
}
}
},250);
break;
//手指移动
case case MotionEvent.ACTION_MOVE:
//如果外部传入回调接口并且,当前状态为长按中
if(mCallBackListener!= null
&& "当前状态"== StateMachine.STATE_LONG_CLICKING){
//接口回调
mCallBackListener.move(event);
}
break;
//手指抬起
case MotionEvent.ACTION_UP:
//如果当前状态为短按,
if (mStateMachine == StateMachine.STATE_SHORT_CLICK){
//则更当前改状态为短按抬起
"当前状态"= StateMachine.STATE_SHORT_UP;
//如果传入了回调接口
if (mCallBackListener != null){
//回调
mCallBackListener.shortClick();
}
//回调结束后,将当前状态切换位初始化
"当前状态"= StateMachine.STATE_INITIA;
}
//如果状态为长按,
if ("当前状态"== StateMachine.STATE_LONG_CLICKING){
//则更改当前状态为长按抬起
"当前状态"= StateMachine.STATE_LONG_UP;
//回调结束
}
break;
}
}
释放资源,关闭循环线程,清空成员变量。
@Override
protected void onDetachedFromWindow() {
//销毁
mStateMachine = null;
if (mPaint != null){
mPaint.reset();
mPaint = null;
}
//mClickIntervalRunnable时判断长短按的Runnable
if (mClickIntervalRunnable!=null){
mClickIntervalRunnable = null;
}
if (mCallBackListener != null){
mCallBackListener = null;
}
if (mPoorThread != null){
mPoorThread.finishPoorThread();
}
super.onDetachedFromWindow();
}
2,Controller层,根据当前状态,修改数据及刷新界面
/**
* 控制数据,重复刷新控件
*/
private final class PoorThread extends Thread{
private static final String TAG = ProgressButtonView.TAG+".PoorThread";
//默认死循环
private boolean "线程开关"= true;
private boolean "是否处于等待状态"= false;
/**
* 刷新间隔ms
*/
private int sleepTime = 13;
PoorThread(){
setName(TAG);
//因为线程实在初始化时new的,当控件状态为初始化时,会保持wait,所以可以直接启动。
start();
}
@Override
public void run() {
//默认死循环,控件销毁时停止
while ("线程开关"){
switch ("当前状态"){
case STATE_INITIA://初始化
case STATE_SHORT_CLICK://短按
case STATE_SHORT_UP://短按抬起
//如果是初始化、短按、短按抬起三种状态,则wait线程
waitPoorThread();
break;
//长按ing
case STATE_LONG_CLICKING:
//长按后,拉伸外圈,收缩内圈,如果外圈半径小于指定最大半径,或内圈半径大于指定最小半径,则修改半径数值,并刷新界面
if ("绘制时使用的外圆半径"< "长按状态外圆半径"|| "绘制时使用的内圆半径"> "长按状态内圆半径"){
//增加外圈半径,每次更改的数值相同
"绘制时使用的外圆半径"+= ( "长按状态外圆半径"- "初始化时外圆半径") / sleepTime;
//确保更改后的数值<=指定最大半径,如果大于,则赋值外圆最大半径
"绘制时使用的外圆半径"= "绘制时使用的外圆半径"> "长按状态外圆半径"? "长按状态外圆半径": "绘制时使用的外圆半径";
//减小内圈半径,每次更改的数值相同
"绘制时使用的内圆半径"+= ("长按状态内圆半径"- "初始化时内圆半径") / sleepTime;
//确保更改后的数值>=指定最小半径,如果小于,则赋值内圈最小半径
"绘制时使用的内圆半径"= "绘制时使用的内圆半径" < "长按状态内圆半径"? "长按状态内圆半径": "绘制时使用的内圆半径" ;
}
//如果当前外圈半径==最大外圈半径,并且内圈半径==最小内圈半径,则说明伸缩动画结束
//如果弧线的角度小于360,则进度条还没加载结束,继续加载进度条
else if ("进度条弧度"< 360){
//则增加弧线角度,确保每次更改数值相同
"进度条弧度"+= 360.0f / "进度条最长时间"* sleepTime;
//确保角度<=360
"进度条弧度"= "进度条弧度">360 ? 360 : "进度条弧度";
}
//拉伸动画结束,弧线角度>=360,则进度条加载结束
else if ("进度条弧度">= 360){
//状态切换为进度条加载结束
"当前状态"= StateMachine.STATE_PROGRESS_OVER;
}
break;
//长按抬手与进度条结束两种状态的处理方式一样,收缩外圈,拉伸内圈,以动画的形式过度到初始状态
case STATE_PROGRESS_OVER://进度条加载结束
case STATE_LONG_UP://长按抬手
//进度条宽度为0,也就相当于不绘制
"进度条弧度"= 0;
//抬手或进度条结束,外圆收缩,内圆拉伸,判断外圆是否大于初始值,内院是否小于初始值,如果是,则更改数据
if ("绘制时使用的外圆半径"> "初始化时外圆半径"|| "绘制时使用的内圆半径" < "初始化时内圆半径"){
//确保外圆半径每次更改的数值相同
"绘制时使用的外圆半径"+= ("初始化时外圆半径" - "长按状态外圆半径") / sleepTime;
//确保外圆半径更改后的数值<=指定数值
"绘制时使用的外圆半径" = "绘制时使用的外圆半径" < "初始化时外圆半径"? "初始化时外圆半径": "绘制时使用的外圆半径" ;
//确保每次更改的数值相同
"绘制时使用的内圆半径"+= ("初始化时内圆半径"- "长按状态内圆半径" ) / sleepTime;
//确保更改后的数值>=指定数值
"绘制时使用的内圆半径"= "绘制时使用的内圆半径"> "初始化时内圆半径"? "初始化时内圆半径": "绘制时使用的内圆半径";
}
//伸缩动画结束
else{
//如果接口不为空
if (mCallBackListener != null){
//如果当前状态为进度条结束
if ("当前状态"== StateMachine.STATE_PROGRESS_OVER){
//状态为进度条结束,回调
mCallBackListener.progressOver();
}else{
//否则就是长按抬手,回调
mCallBackListener.longClickUp();
}
}
//如果内外圆都恢复为初始大小,则将状态切换为初始状态
"当前状态"= StateMachine.STATE_INITIA;
}
break;
}
try {
sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
//刷新界面
postInvalidate();
}
}
/**
* 销毁线程
*/
public void finishPoorThread(){
"线程开关"= false;
noitfyPoorThread();
}
//线程等待
public void waitPoorThread(){
if ("是否处于等待状态") return;
synchronized (PoorThread.this){
try {
"是否处于等待状态"= true;
//等待
PoorThread.this.wait();
} catch (InterruptedException e) {
"是否处于等待状态" = false;
e.printStackTrace();
}
}
}
//线程唤醒
public void noitfyPoorThread(){
if (!"是否处于等待状态") return;
synchronized (PoorThread.this){
//唤醒
PoorThread.this.notify();
"是否处于等待状态"= false;
}
}
}
网友评论