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>
番茄钟的相关属性可以根据需要自行设置。