Android:自定义View之番茄钟

闲来无事,回顾了一下之前写的项目,把番茄钟从里面整理出来了。

该View通过上下滑动设置倒计时的时间,调用start()方法开始倒计时,stop()方法停止计时。

效果图如下:

核心代码:

  1 import android.animation.ValueAnimator;
  2 import android.content.Context;
  3 import android.graphics.Canvas;
  4 import android.graphics.Color;
  5 import android.graphics.Paint;
  6 import android.graphics.RectF;
  7 import android.os.Build;
  8 import android.os.CountDownTimer;
  9 import android.util.AttributeSet;
 10 import android.util.DisplayMetrics;
 11 import android.util.Log;
 12 import android.view.MotionEvent;
 13 import android.view.View;
 14 import android.view.ViewGroup;
 15 import android.view.WindowManager;
 16 
 17 import androidx.annotation.Nullable;
 18 import androidx.annotation.RequiresApi;
 19 
 20 public class TomatoClockView extends View {
 21     private static final String TAG = "TomatoClockView";
 22     
 23     private Paint arcPaint;  //圆弧画笔
 24     private Paint textPaint;  //时间文本画笔
 25     private int backgroundColor = Color.parseColor("#D1D1D1");
 26     private int arcColor = Color.BLUE;
 27 
 28     private int width;  //View的宽
 29     private int height;  //View的高
 30     private float centerX;  //View中心点的X坐标
 31     private float centerY;  //View中心点的Y坐标
 32 
 33     private float oldOffsetY;  //上一次MOVE事件结束位置和DOWN事件落点之间Y坐标的偏移量
 34     private float offsetY;  //本次MOVE事件结束位置和DOWN事件落点之间Y坐标的偏移量
 35     float touchedY;  //本次DOWN事件落点的Y坐标
 36 
 37     private static final int MAX_TIME = 60;  //最大倒计时长
 38     private static String textTime = "00:00";  //时间文本
 39     private long countDownTime;  //倒计时时长(毫秒)
 40     private float time;  //倒计时时长(分钟)
 41 
 42     private float sweepVelocity = 0;  //动画执行的完成度
 43     private ValueAnimator valueAnimator;  //属性动画对象
 44 
 45     private boolean isStarted;  //倒计时是否已开始
 46 
 47     private MyTimer mTimeCounter;  //倒计时器
 48 
 49     private class MyTimer extends CountDownTimer{
 50 
 51         /**
 52          * @param millisInFuture    The number of millis in the future from the call
 53          *                          to {@link #start()} until the countdown is done and {@link #onFinish()}
 54          *                          is called.
 55          * @param countDownInterval The interval along the way to receive
 56          *                          {@link #onTick(long)} callbacks.
 57          */
 58         public MyTimer(long millisInFuture, long countDownInterval) {
 59             super(millisInFuture, countDownInterval);
 60         }
 61 
 62         @Override
 63         public void onTick(long millisUntilFinished) {
 64 
 65             textTime = formatCountTime(millisUntilFinished);
 66             invalidate();
 67 
 68         }
 69 
 70         @Override
 71         public void onFinish() {
 72             textTime = "00:00";
 73             invalidate();
 74         }
 75     }
 76 
 77     //view是在JAVA代码中new的,则调用此构造函数
 78     public TomatoClockView(Context context) {
 79         super(context);
 80         init();
 81     }
 82 
 83     //View是在.xml文件中声明的,则调用此构造函数
 84     public TomatoClockView(Context context, @Nullable AttributeSet attrs) {
 85         super(context, attrs);
 86         init();
 87     }
 88 
 89     public TomatoClockView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
 90         super(context, attrs, defStyleAttr);
 91         init();
 92     }
 93 
 94     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 95     public TomatoClockView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
 96         super(context, attrs, defStyleAttr, defStyleRes);
 97     }
 98 
 99     @Override
100     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
101         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
102 
103         width = MeasureSpec.getSize(widthMeasureSpec);
104         height = MeasureSpec.getSize(heightMeasureSpec);
105 
106         //定义LayoutParams为warp_content时的测量宽高,否则wrap_content会失效
107         if(getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT
108         && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
109             width = 400;
110             height = 500;
111         } else if(getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){
112             width = 400;
113         } else if(getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
114             height = 400;
115         }
116 
117 
118         //计算番茄钟的中心点
119         centerX = getLeft() + width/2;
120         centerY = getTop() + height/2;
121 
122         setMeasuredDimension(width, height);  //保存测量宽高
123         Log.d(TAG, "onMeasure: ");
124     }
125 
126     @Override
127     protected void onDraw(Canvas canvas) {
128         super.onDraw(canvas);
129 
130         //对padding进行处理,否则padding属性将会失效
131         int paddingLeft = getPaddingLeft();
132         int paddingRight = getPaddingRight();
133         int paddingTop = getPaddingTop();
134         int paddingBottom = getPaddingBottom();
135 
136         //int radius = Math.min(width-paddingLeft-paddingRight, height-paddingTop-paddingBottom)/2;  //计算半径
137 
138         RectF rectF = new RectF();
139         rectF.set(centerX-width/2 + paddingLeft, centerY-height/2 + paddingTop, centerX+width/2 - paddingRight, centerY+height/2 - paddingBottom);
140 
141         //绘制底部圆弧
142         canvas.save();
143         arcPaint.setColor(backgroundColor);
144         canvas.drawArc(rectF, -90, 360, false, arcPaint);
145         canvas.restore();
146 
147         //绘制倒计时圆弧
148         canvas.save();
149         arcPaint.setColor(arcColor);
150         canvas.drawArc(rectF, -90, 360 * sweepVelocity, false, arcPaint);
151         canvas.restore();
152 
153         //绘制时间文本
154         canvas.save();
155         Paint.FontMetrics metrics = textPaint.getFontMetrics();
156         float baseline = (metrics.bottom - metrics.top)/2 + centerY - metrics.bottom;
157         canvas.drawText(textTime, centerX, baseline, textPaint );
158         canvas.restore();
159     }
160 
161     @Override
162     public boolean onTouchEvent(MotionEvent event) {
163         if(isStarted){
164             return true;
165         }
166 
167         //获取屏幕高度
168         WindowManager manager = (WindowManager) (getContext().getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
169         DisplayMetrics metrics = new DisplayMetrics();
170         manager.getDefaultDisplay().getMetrics(metrics);
171         float screenHeight = metrics.heightPixels;
172 
173         float y = event.getY();  //获取触摸事件发生位置的y坐标
174 
175         //通过上下滑动来设置倒计时时间
176         //原理:MOVE事件结束时的y坐标 - DOWN事件发生的y坐标,MAX_TIME*(所得值/屏幕高度)即为倒计时时间,负减正增
177         switch (event.getAction()){
178 
179             case MotionEvent.ACTION_DOWN:
180                 touchedY = y;
181                 break;
182 
183             case MotionEvent.ACTION_MOVE:
184                 offsetY = y - touchedY;
185 
186                 //可以通过多次滑动来调整时间
187                 float totalOffsetY = oldOffsetY + offsetY;
188                 if(totalOffsetY <= 0){
189                     totalOffsetY = 0;
190                 } else if(totalOffsetY >= screenHeight){
191                     totalOffsetY = screenHeight;
192                 }
193 
194                 time = totalOffsetY/screenHeight*MAX_TIME;  //分钟
195 
196                 textTime = formatTime((long)time);
197 
198                 invalidate();
199                 break;
200 
201             case MotionEvent.ACTION_UP:
202                 oldOffsetY = offsetY;  //记录上次滑动的位移量,用以实现多次滑动调整时间
203                 countDownTime = (long)time * 60 * 1000;  //倒计时时长,毫秒
204                 break;
205         }
206 
207         return true;
208     }
209 
210     private void init(){
211         initPaint();
212         initValueAnimation();
213     }
214 
215     private void initPaint(){
216         arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
217         arcPaint.setStyle(Paint.Style.STROKE);  //描边
218         arcPaint.setStrokeWidth(30);
219 
220 
221         textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
222         textPaint.setColor(Color.BLACK);
223         textPaint.setStrokeWidth(20);
224         textPaint.setTextSize(180);
225         textPaint.setTextAlign(Paint.Align.CENTER);
226     }
227 
228     private void initValueAnimation(){
229         valueAnimator = ValueAnimator.ofFloat(0f, 1f);
230         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
231             @Override
232             public void onAnimationUpdate(ValueAnimator animation) {
233                 sweepVelocity = (float) animation.getAnimatedValue();
234                 invalidate();
235             }
236         });
237     }
238 
239     /**
240      * 开始倒计时
241      */
242     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
243     public void start(){
244         if(!isStarted){
245             isStarted = true;
246 
247             //设置动画时间并开始动画
248             valueAnimator.setDuration(countDownTime);
249             valueAnimator.start();
250 
251             //设置倒计时时间并开始倒计时
252             mTimeCounter = new MyTimer(countDownTime, 1000);
253             mTimeCounter.start();
254         }
255 
256     }
257 
258     /**
259      * 停止倒计时
260      */
261     public void stop(){
262         mTimeCounter.cancel();
263         valueAnimator.end();
264 
265         isStarted = false;
266         time = 0f;
267         textTime = "00:00";
268         sweepVelocity = 0;
269         oldOffsetY = 0;
270 
271         invalidate();
272     }
273 
274     /**
275      * 倒计时开始前格式化时间文本
276      * @param time
277      * @return
278      */
279     private String formatTime(long time){
280         StringBuilder sb = new StringBuilder();
281 
282         if(time < 10){
283             sb.append("0" + time + ":00");
284         } else {
285             sb.append(time + ":00");
286         }
287 
288         return sb.toString();
289     }
290 
291     /**
292      * 在倒计时过程中格式化时间文本
293      * @param time
294      * @return
295      */
296     private static String formatCountTime(long time){
297 
298         StringBuilder sb = new StringBuilder();
299 
300         time = time/1000;  //毫秒转秒
301 
302         long min = time/60;  //分钟
303         long second = time - min*60;  //
304 
305         if(min < 10){
306             sb.append("0" + min + ":");
307         } else {
308             sb.append(min + ":");
309         }
310 
311         if(second < 10){
312             sb.append("0" + second);
313         } else {
314             sb.append(second);
315         }
316 
317 
318         return sb.toString();
319     }
320 
321     public boolean isStarted() {
322         return isStarted;
323     }
324 
325     public void setStarted(boolean started) {
326         isStarted = started;
327     }
328 }

 

在布局文件中直接调用即可:

<androidx.appcompat.widget.LinearLayoutCompat
        android:layout_centerInParent="true"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.example.viewtest.view.TomatoClockView <--注意要使用全限定名!-->
            android:id="@+id/m_view"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:layout_gravity="center"
            android:padding="10dp" />

        <Button
            android:id="@+id/btn_start_clock"
            android:layout_margin="20dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@id/m_view"
            android:text="START" />

    </androidx.appcompat.widget.LinearLayoutCompat>

番茄钟的相关属性可以根据需要自行设置。

 

posted @ 2021-03-15 23:02  凤青  阅读(362)  评论(0编辑  收藏  举报