我正在开发一个应用程序,只是构建了它的逻辑部分。现在我想设计这个应用程序,就像著名的计时器应用程序一样。例如:
我想要的是外圈,它充满了某个事件的每个触发器或数字的增加。我实际上不知道它是动画部分(比如内置在 Flash 中或什么),或者只是通过使用其内置属性和功能在 android 本身中编码来实现。因此,如果有人告诉我,请向我解释使用了哪些工具或任何可以从底层解释事情的参考教程。我真的不懂设计。这有什么代码吗??
我正在开发一个应用程序,只是构建了它的逻辑部分。现在我想设计这个应用程序,就像著名的计时器应用程序一样。例如:
我想要的是外圈,它充满了某个事件的每个触发器或数字的增加。我实际上不知道它是动画部分(比如内置在 Flash 中或什么),或者只是通过使用其内置属性和功能在 android 本身中编码来实现。因此,如果有人告诉我,请向我解释使用了哪些工具或任何可以从底层解释事情的参考教程。我真的不懂设计。这有什么代码吗??
这会吗?
更新:现在也正确处理现实世界的时间。
示例截图:
代码:
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextPaint;
import android.view.View;
import android.graphics.*;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new CircularCountdown(this));
}
private static class CircularCountdown extends View {
private final Paint backgroundPaint;
private final Paint progressPaint;
private final Paint textPaint;
private long startTime;
private long currentTime;
private long maxTime;
private long progressMillisecond;
private double progress;
private RectF circleBounds;
private float radius;
private float handleRadius;
private float textHeight;
private float textOffset;
private final Handler viewHandler;
private final Runnable updateView;
public CircularCountdown(Context context) {
super(context);
// used to fit the circle into
circleBounds = new RectF();
// size of circle and handle
radius = 200;
handleRadius = 10;
// limit the counter to go up to maxTime ms
maxTime = 5000;
// start and current time
startTime = System.currentTimeMillis();
currentTime = startTime;
// the style of the background
backgroundPaint = new Paint();
backgroundPaint.setStyle(Paint.Style.STROKE);
backgroundPaint.setAntiAlias(true);
backgroundPaint.setStrokeWidth(10);
backgroundPaint.setStrokeCap(Paint.Cap.SQUARE);
backgroundPaint.setColor(Color.parseColor("#4D4D4D")); // dark gray
// the style of the 'progress'
progressPaint = new Paint();
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setAntiAlias(true);
progressPaint.setStrokeWidth(10);
progressPaint.setStrokeCap(Paint.Cap.SQUARE);
progressPaint.setColor(Color.parseColor("#00A9FF")); // light blue
// the style for the text in the middle
textPaint = new TextPaint();
textPaint.setTextSize(radius / 2);
textPaint.setColor(Color.BLACK);
textPaint.setTextAlign(Paint.Align.CENTER);
// text attributes
textHeight = textPaint.descent() - textPaint.ascent();
textOffset = (textHeight / 2) - textPaint.descent();
// This will ensure the animation will run periodically
viewHandler = new Handler();
updateView = new Runnable(){
@Override
public void run(){
// update current time
currentTime = System.currentTimeMillis();
// get elapsed time in milliseconds and clamp between <0, maxTime>
progressMillisecond = (currentTime - startTime) % maxTime;
// get current progress on a range <0, 1>
progress = (double) progressMillisecond / maxTime;
CircularCountdown.this.invalidate();
viewHandler.postDelayed(updateView, 1000/60);
}
};
viewHandler.post(updateView);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// get the center of the view
float centerWidth = canvas.getWidth() / 2;
float centerHeight = canvas.getHeight() / 2;
// set bound of our circle in the middle of the view
circleBounds.set(centerWidth - radius,
centerHeight - radius,
centerWidth + radius,
centerHeight + radius);
// draw background circle
canvas.drawCircle(centerWidth, centerHeight, radius, backgroundPaint);
// we want to start at -90°, 0° is pointing to the right
canvas.drawArc(circleBounds, -90, (float)(progress*360), false, progressPaint);
// display text inside the circle
canvas.drawText((double)(progressMillisecond/100)/10 + "s",
centerWidth,
centerHeight + textOffset,
textPaint);
// draw handle or the circle
canvas.drawCircle((float)(centerWidth + (Math.sin(progress * 2 * Math.PI) * radius)),
(float)(centerHeight - (Math.cos(progress * 2 * Math.PI) * radius)),
handleRadius,
progressPaint);
}
}
}
的解决方案Antimonit
有两个重大问题:
circular clock view
当您使用 破坏活动/片段并再次显示时钟时,会发生内存泄漏。基于Antimonit
代码(谢谢!),我创建了更多可重用和内存安全的解决方案。现在,几乎所有的参数都可以从XML
文件中设置。在活动/片段类的最后,我们需要调用startCount
方法。removeCallbacks
当活动/片段将被销毁以避免内存泄漏时,我强烈推荐调用方法。
KakaCircularCounter.java 类:
public class KakaCircularCounter extends View {
public static final int DEF_VALUE_RADIUS = 250;
public static final int DEF_VALUE_EDGE_WIDTH = 15;
public static final int DEF_VALUE_TEXT_SIZE = 18;
private Paint backgroundPaint;
private Paint progressPaint;
private Paint textPaint;
private RectF circleBounds;
private long startTime;
private long currentTime;
private long maxTime;
private long progressMillisecond;
private double progress;
private float radius;
private float edgeHeadRadius;
private float textInsideOffset;
private KakaDirectionCount countDirection;
private Handler viewHandler;
private Runnable updateView;
public KakaCircularCounter(Context context) {
super(context);
init(null);
}
public KakaCircularCounter(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public KakaCircularCounter(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
public KakaCircularCounter(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}
private void init(AttributeSet attrSet) {
if (attrSet == null) {
return;
}
TypedArray typedArray = getContext().obtainStyledAttributes(attrSet, R.styleable.KakaCircularCounter);
circleBounds = new RectF();
backgroundPaint = setupBackground(typedArray);
progressPaint = setupProgress(typedArray);
textPaint = setupText(typedArray);
textInsideOffset = (textPaint.descent() - textPaint.ascent() / 2) - textPaint.descent();
radius = typedArray.getDimensionPixelSize(R.styleable.KakaCircularCounter_clockRadius, DEF_VALUE_RADIUS);
edgeHeadRadius = typedArray.getDimensionPixelSize(R.styleable.KakaCircularCounter_edgeHeadRadius, DEF_VALUE_EDGE_WIDTH);
countDirection = KakaDirectionCount.values()[typedArray.getInt(R.styleable.KakaCircularCounter_countFrom,
KakaDirectionCount.MAXIMUM.ordinal())];
typedArray.recycle();
}
private Paint setupText(TypedArray typedArray) {
Paint t = new Paint();
t.setTextSize(typedArray.getDimensionPixelSize(R.styleable.KakaCircularCounter_textInsideSize, DEF_VALUE_TEXT_SIZE));
t.setColor(typedArray.getColor(R.styleable.KakaCircularCounter_textInsideColor, Color.BLACK));
t.setTextAlign(Paint.Align.CENTER);
return t;
}
private Paint setupProgress(TypedArray typedArray) {
Paint p = new Paint();
p.setStyle(Paint.Style.STROKE);
p.setAntiAlias(true);
p.setStrokeCap(Paint.Cap.SQUARE);
p.setStrokeWidth(typedArray.getDimensionPixelSize(R.styleable.KakaCircularCounter_clockWidth, DEF_VALUE_EDGE_WIDTH));
p.setColor(typedArray.getColor(R.styleable.KakaCircularCounter_edgeBackground, Color.parseColor("#4D4D4D")));
return p;
}
private Paint setupBackground(TypedArray ta) {
Paint b = new Paint();
b.setStyle(Paint.Style.STROKE);
b.setStrokeWidth(ta.getDimensionPixelSize(R.styleable.KakaCircularCounter_clockWidth, DEF_VALUE_EDGE_WIDTH));
b.setColor(ta.getColor(R.styleable.KakaCircularCounter_clockBackground, Color.parseColor("#4D4D4D")));
b.setAntiAlias(true);
b.setStrokeCap(Paint.Cap.SQUARE);
return b;
}
public void startCount(long maxTimeInMs) {
startTime = System.currentTimeMillis();
this.maxTime = maxTimeInMs;
viewHandler = new Handler();
updateView = () -> {
currentTime = System.currentTimeMillis();
progressMillisecond = (currentTime - startTime) % maxTime;
progress = (double) progressMillisecond / maxTime;
KakaCircularCounter.this.invalidate();
viewHandler.postDelayed(updateView, 1000 / 60);
};
viewHandler.post(updateView);
}
public void removeCallbacks() {
viewHandler.removeCallbacks(updateView);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float centerWidth = getWidth() / 2f;
float centerHeight = getHeight() / 2f;
circleBounds.set(centerWidth - radius,
centerHeight - radius,
centerWidth + radius,
centerHeight + radius);
canvas.drawCircle(centerWidth, centerHeight, radius, backgroundPaint);
canvas.drawArc(circleBounds, -90, (float) (progress * 360), false, progressPaint);
canvas.drawText(getTextToDraw(),
centerWidth,
centerHeight + textInsideOffset,
textPaint);
canvas.drawCircle((float) (centerWidth + (Math.sin(progress * 2 * Math.PI) * radius)),
(float) (centerHeight - (Math.cos(progress * 2 * Math.PI) * radius)),
edgeHeadRadius,
progressPaint);
}
@NonNull
private String getTextToDraw() {
if (countDirection.equals(KakaDirectionCount.ZERO)) {
return String.valueOf(progressMillisecond / 1000);
} else {
return String.valueOf((maxTime - progressMillisecond) / 1000);
}
}
}
KakaDirectionCount 枚举:
public enum KakaDirectionCount {
ZERO, MAXIMUM
}
值目录中的属性文件 (kaka_circular_counter.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="KakaCircularCounter">
<attr name="clockRadius" format="dimension"/>
<attr name="clockBackground" format="color"/>
<attr name="clockWidth" format="dimension"/>
<attr name="edgeBackground" format="color"/>
<attr name="edgeWidth" format="dimension"/>
<attr name="edgeHeadRadius" format="dimension"/>
<attr name="textInsideSize" format="dimension"/>
<attr name="textInsideColor" format="color"/>
<attr name="countFrom" format="enum">
<enum name="ZERO" value="0"/>
<enum name="MAXIMUM" value="1"/>
</attr>
</declare-styleable>
</resources>
在 xml 文件中使用的示例:
<pl.kaka.KakaCircularCounter
android:id="@+id/circular_counter"
android:layout_width="180dp"
android:layout_height="180dp"
app:layout_constraintBottom_toBottomOf="@id/backgroundTriangle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/backgroundTriangle"
app:clockRadius="85dp"
app:clockBackground="@color/colorTransparent"
app:clockWidth="3dp"
app:edgeBackground="@color/colorAccentSecondary"
app:edgeWidth="5dp"
app:edgeHeadRadius="1dp"
app:textInsideSize="60sp"
app:textInsideColor="@color/colorWhite"
app:countFrom="MAXIMUM"/>
在活动或片段中使用的示例:
//the number in parameter is the value of the counted time
binding.circularCounter.startCount(12000);
注意:记得在销毁活动/片段时删除回调,因为发生内存泄漏。例如:
@Override
public void onDestroyView() {
super.onDestroyView();
binding.circularCounter.removeCallbacks();
}