美文网首页Custom ViewsAndroid知识手机移动程序开发
自定义View,微信视频录制进度条按钮

自定义View,微信视频录制进度条按钮

作者: ImmortalHalfWu | 来源:发表于2017-01-22 15:34 被阅读209次
效果图.gif 效果图.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,外圈拉伸结束并且内圈收缩结束,绘制进度条,动态。


样式图.PNG

按钮共有六种事件驱动:
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;
            }
  }

}

相关文章

网友评论

    本文标题:自定义View,微信视频录制进度条按钮

    本文链接:https://www.haomeiwen.com/subject/otwvbttx.html