自定义一个广告倒计时 View

4,635 阅读5分钟

上一篇《源码解析:SwipeRefreshLayout是如何工作的(上)》,这篇文章写起来很累,因为本身看源码就是一个体力活,也是技术活,写得很好,推荐大家点进去看一看,不要害怕看源码!!!SwipeRefreshLayout是如何工作的(下),这篇文章有时间再写吧,最近一直加班,没时间去看源码。

今天打开迅雷手机客户端准备看片的时候,无意间发现这个自定义View,感觉很好看的,实现起来也不麻烦,就尝试着模仿了一下,花了一天,最后终于搞出来了。因为技术比较菜,所以时间有点长,总之慢慢来吧。


迅雷截图


自定义View效果图

  1. 自定义属性
    底盘的颜色
    进度条的颜色
    进度条粗细
    文字内容
    文字颜色
    文字大小

     
         
         
         
         
         
         
     
  2. 自定义一个CountDownView,继承View

    public class CountDownView extends View {
    
     private static final String TAG = CountDownView.class.getSimpleName();
     private static final int BACKGROUND_COLOR = 0x50555555;
     private static final float BORDER_WIDTH = 15f;
     private static final int BORDER_COLOR = 0xFF6ADBFE;
     private static final String TEXT = "跳过广告";
     private static final float TEXT_SIZE = 50f;
     private static final int TEXT_COLOR = 0xFFFFFFFF;
    
     private int backgroundColor;
     private float borderWidth;
     private int borderColor;
     private String text;
     private int textColor;
     private float textSize;
    
     private Paint circlePaint;
     private TextPaint textPaint;
     private Paint borderPaint;
    
     private float progress = 135;
     private StaticLayout staticLayout;
    
     private CountDownTimerListener listener;
    
     public CountDownView(Context context) {
         this(context, null);
     }
    
     public CountDownView(Context context, AttributeSet attrs) {
         super(context, attrs);
         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CountDownView);
         backgroundColor = ta.getColor(R.styleable.CountDownView_background_color, BACKGROUND_COLOR);
         borderWidth = ta.getDimension(R.styleable.CountDownView_border_width, BORDER_WIDTH);
         borderColor = ta.getColor(R.styleable.CountDownView_border_color, BORDER_COLOR);
         text = ta.getString(R.styleable.CountDownView_text);
         if (text == null) {
             text = TEXT;
         }
         textSize = ta.getDimension(R.styleable.CountDownView_text_size, TEXT_SIZE);
         textColor = ta.getColor(R.styleable.CountDownView_text_color, TEXT_COLOR);
         ta.recycle();
         init();
     }
    
     private void init() {
         circlePaint = new Paint();
         circlePaint.setAntiAlias(true);
         circlePaint.setDither(true);
         circlePaint.setColor(backgroundColor);
         circlePaint.setStyle(Paint.Style.FILL);
    
         textPaint = new TextPaint();
         textPaint.setAntiAlias(true);
         textPaint.setDither(true);
         textPaint.setColor(textColor);
         textPaint.setTextSize(textSize);
         textPaint.setTextAlign(Paint.Align.CENTER);
    
         borderPaint = new Paint();
         borderPaint.setAntiAlias(true);
         borderPaint.setDither(true);
         borderPaint.setColor(borderColor);
         borderPaint.setStrokeWidth(borderWidth);
         borderPaint.setStyle(Paint.Style.STROKE);
     }
    }

    重写了两个构造方法,然后对自定义属性进行了初始化

  3. 重写onMeasure方法

     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         int width = MeasureSpec.getSize(widthMeasureSpec);
         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
         int height = MeasureSpec.getSize(heightMeasureSpec);
         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
         if (widthMode != MeasureSpec.EXACTLY) {
             width = staticLayout.getWidth();
         }
         if (heightMode != MeasureSpec.EXACTLY) {
             height = staticLayout.getHeight();
         }
         setMeasuredDimension(width, height);
     }

    这个不多说,重写onMeasure方法是必须实现的,重写此方法的目的是测量控件的实际大小,因为有的时候用户填写的width和height是wrap_content,懂了吧,当wrap_content的时候,我们就需要测量控件的实际大小了

  4. 重写onDraw方法

     @Override
     protected void onDraw(Canvas canvas) {
         int width = getMeasuredWidth();
         int height = getMeasuredHeight();
         int min = Math.min(width, height);
         //画底盘
         canvas.drawCircle(width / 2, height / 2, min / 2, circlePaint);
         //画边框
         RectF rectF;
         if (width > height) {
             rectF = new RectF(width / 2 - min / 2 + borderWidth / 2, 0 + borderWidth / 2, width / 2 + min / 2 - borderWidth / 2, height - borderWidth / 2);
         } else {
             rectF = new RectF(borderWidth / 2, height / 2 - min / 2 + borderWidth / 2, width - borderWidth / 2, height / 2 - borderWidth / 2 + min / 2);
         }
         canvas.drawArc(rectF, -90, progress, false, borderPaint);
         //画居中的文字
         canvas.translate(width / 2, height / 2 - staticLayout.getHeight() / 2);
         staticLayout.draw(canvas);
     }

    这里有必要提一下的是StaticLayout这个类。如果我们用canvas.drawText这个方法,也是可以的,但是有个问题,这个方法写出来的文字是单行的,不会回行显示,但是迅雷中的“跳过广告”4个字是分两行显示的,这个时候我们就需要用到StaticLayout这个类了。这个类使用起来也很简单,具体的使用方法请参照其它博客。

其实到这里,整个控件已经写完了,但是我们希望这个控件在开始计时的时候给我们一个提示,在结束的时候再给我们一个提示,好让我们进行额外的操作。
我们的解决办法是给外界暴露一个接口,直接看代码吧!

    public void start() {
        if (listener != null) {
            listener.onStartCount();
        }
        CountDownTimer countDownTimer = new CountDownTimer(3600, 36) {
            @Override
            public void onTick(long millisUntilFinished) {
                progress = ((3600 - millisUntilFinished) / 3600f) * 360;
                Log.d(TAG, "progress:" + progress);
                invalidate();
            }

            @Override
            public void onFinish() {
                progress = 360;
                invalidate();
                if (listener != null) {
                    listener.onFinishCount();
                }
            }
        }.start();
    }

    public void setCountDownTimerListener(CountDownTimerListener listener) {
        this.listener = listener;
    }

    public interface CountDownTimerListener {

        void onStartCount();

        void onFinishCount();
    }

我们定义了一个接口,里面有两个方法,onStartCount()和onFinishCount()
public void start()这个方法是用来启动计时器的,调用这个方法之后,计时程序就会开始了,开始的时候会调用onStartCount这个接口,然后计时的过程中会根据process(进度)不断地重绘整个View,达到动画效果,最后结束的时候会调用onFinishCount这个接口

看一下整体的代码吧:

package com.example.customview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.CountDownTimer;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Created on 2016/10/8.
 */

public class CountDownView extends View {

    private static final String TAG = CountDownView.class.getSimpleName();
    private static final int BACKGROUND_COLOR = 0x50555555;
    private static final float BORDER_WIDTH = 15f;
    private static final int BORDER_COLOR = 0xFF6ADBFE;
    private static final String TEXT = "跳过广告";
    private static final float TEXT_SIZE = 50f;
    private static final int TEXT_COLOR = 0xFFFFFFFF;

    private int backgroundColor;
    private float borderWidth;
    private int borderColor;
    private String text;
    private int textColor;
    private float textSize;

    private Paint circlePaint;
    private TextPaint textPaint;
    private Paint borderPaint;

    private float progress = 0;
    private StaticLayout staticLayout;

    private CountDownTimerListener listener;

    public CountDownView(Context context) {
        this(context, null);
    }

    public CountDownView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CountDownView);
        backgroundColor = ta.getColor(R.styleable.CountDownView_background_color, BACKGROUND_COLOR);
        borderWidth = ta.getDimension(R.styleable.CountDownView_border_width, BORDER_WIDTH);
        borderColor = ta.getColor(R.styleable.CountDownView_border_color, BORDER_COLOR);
        text = ta.getString(R.styleable.CountDownView_text);
        if (text == null) {
            text = TEXT;
        }
        textSize = ta.getDimension(R.styleable.CountDownView_text_size, TEXT_SIZE);
        textColor = ta.getColor(R.styleable.CountDownView_text_color, TEXT_COLOR);
        ta.recycle();
        init();
    }

    private void init() {
        circlePaint = new Paint();
        circlePaint.setAntiAlias(true);
        circlePaint.setDither(true);
        circlePaint.setColor(backgroundColor);
        circlePaint.setStyle(Paint.Style.FILL);

        textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setDither(true);
        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
        textPaint.setTextAlign(Paint.Align.CENTER);

        borderPaint = new Paint();
        borderPaint.setAntiAlias(true);
        borderPaint.setDither(true);
        borderPaint.setColor(borderColor);
        borderPaint.setStrokeWidth(borderWidth);
        borderPaint.setStyle(Paint.Style.STROKE);

        int textWidth = (int) textPaint.measureText(text.substring(0, (text.length() + 1) / 2));
        staticLayout = new StaticLayout(text, textPaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 1F, 0, false);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode != MeasureSpec.EXACTLY) {
            width = staticLayout.getWidth();
        }
        if (heightMode != MeasureSpec.EXACTLY) {
            height = staticLayout.getHeight();
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        int min = Math.min(width, height);
        //画底盘
        canvas.drawCircle(width / 2, height / 2, min / 2, circlePaint);
        //画边框
        RectF rectF;
        if (width > height) {
            rectF = new RectF(width / 2 - min / 2 + borderWidth / 2, 0 + borderWidth / 2, width / 2 + min / 2 - borderWidth / 2, height - borderWidth / 2);
        } else {
            rectF = new RectF(borderWidth / 2, height / 2 - min / 2 + borderWidth / 2, width - borderWidth / 2, height / 2 - borderWidth / 2 + min / 2);
        }
        canvas.drawArc(rectF, -90, progress, false, borderPaint);
        //画居中的文字
//       canvas.drawText("稍等片刻", width / 2, height / 2 - textPaint.descent() + textPaint.getTextSize() / 2, textPaint);
        canvas.translate(width / 2, height / 2 - staticLayout.getHeight() / 2);
        staticLayout.draw(canvas);
    }

    public void start() {
        if (listener != null) {
            listener.onStartCount();
        }
        CountDownTimer countDownTimer = new CountDownTimer(3600, 36) {
            @Override
            public void onTick(long millisUntilFinished) {
                progress = ((3600 - millisUntilFinished) / 3600f) * 360;
                Log.d(TAG, "progress:" + progress);
                invalidate();
            }

            @Override
            public void onFinish() {
                progress = 360;
                invalidate();
                if (listener != null) {
                    listener.onFinishCount();
                }
            }
        }.start();
    }

    public void setCountDownTimerListener(CountDownTimerListener listener) {
        this.listener = listener;
    }

    public interface CountDownTimerListener {

        void onStartCount();

        void onFinishCount();
    }
}

这次比较懒,没有写什么注释
最后,我们来看看如何使用这个自定义View,我们现在布局文件中引用这个布局

    

定义了宽度,高度,背景色,边框颜色,边框粗细,文字大小
然后到MainActivity中去使用这个自定义View

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private long lastTime;

    private CountDownView count_down_view;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        count_down_view = (CountDownView) findViewById(R.id.countDownView);
        count_down_view.setCountDownTimerListener(new CountDownView.CountDownTimerListener() {
            @Override
            public void onStartCount() {
                Toast.makeText(getApplicationContext(),"开始计时",Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onFinishCount() {
                Toast.makeText(getApplicationContext(),"计时结束",Toast.LENGTH_SHORT).show();
            }
        });
        count_down_view.setOnClickListener(this);
    }

    //连按两次退出应用程序
    @Override
    public void onBackPressed() {
        long currentTime = System.currentTimeMillis();
        if (currentTime - lastTime < 2 * 1000) {
            super.onBackPressed();
        } else {
            Toast.makeText(this, "请再按一次", Toast.LENGTH_SHORT).show();
            lastTime = currentTime;
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.countDownView:
                count_down_view.start();
                break;
        }
    }
}

中间穿插着一些连按两次Back键退出应用程序的代码,所以代码量比较多,其实使用起来还是很方便的

效果图已经在文章开头发了,gif我就不发了,因为不会弄。

如果觉得我写得好,可以点个赞,关注一下哦~另外,我的微信公众号也开播了,每天都会分享一篇优质的技术文章,欢迎大家来踩点。(公众号:代码也是人)


代码也是人