Effect
First look at the effect:
the above is the effect of the gif image, just for reference, the actual speed is much faster, which can better reflect the effect of the breathing light.
Custom View
BreathView
The custom Kotlin code is as follows:
import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
class BreathView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), AnimatorUpdateListener {
private val mCenterCircleRadius: Float
private var mMaxCircleRadius: Float
private val mCirclePaint: Paint
private var mAlphaValue = 0
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.BreathView)
mCenterCircleRadius = a.getDimension(R.styleable.BreathView_centerCircleRadius, 5f)
mMaxCircleRadius = a.getDimension(R.styleable.BreathView_maxCircleRadius, 10f)
if (mCenterCircleRadius >= mMaxCircleRadius) {
mMaxCircleRadius = mCenterCircleRadius * 2
}
val circleColor = a.getColor(R.styleable.BreathView_circleColor, Color.GREEN)
a.recycle()
mCirclePaint = Paint()
mCirclePaint.isAntiAlias = true
mCirclePaint.style = Paint.Style.FILL
mCirclePaint.color = circleColor
val circleAlphaValueAnimator = ValueAnimator.ofInt(0, 255)
circleAlphaValueAnimator.duration = BREATH_TIME
circleAlphaValueAnimator.repeatCount = Animation.INFINITE
circleAlphaValueAnimator.repeatMode = ValueAnimator.REVERSE
circleAlphaValueAnimator.interpolator = LinearInterpolator()
circleAlphaValueAnimator.addUpdateListener(this)
circleAlphaValueAnimator.start()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var width = MeasureSpec.getSize(widthMeasureSpec)
var height = MeasureSpec.getSize(heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(widthMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
width = Math.max(width.toFloat(), mMaxCircleRadius * 2).toInt()
}
if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
height = Math.max(width.toFloat(), mMaxCircleRadius * 2).toInt()
}
setMeasuredDimension(width, height)
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
val centerX = width / 2.0f
val centerY = height / 2.0f
mCirclePaint.alpha = 255
canvas.drawCircle(centerX, centerY, mCenterCircleRadius, mCirclePaint)
mCirclePaint.alpha = mAlphaValue
canvas.drawCircle(centerX, centerY, mMaxCircleRadius, mCirclePaint)
}
override fun onAnimationUpdate(valueAnimator: ValueAnimator) {
mAlphaValue = valueAnimator.animatedValue as Int
invalidate()
}
companion object {
private const val BREATH_TIME: Long = 1000 //动画执行时间/呼吸速率
}
}
BreathView
The custom Java code is as follows:
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
public class BreathView extends View implements ValueAnimator.AnimatorUpdateListener {
private static final long BREATH_TIME = 1000; //动画执行时间/呼吸速率
private final float mCenterCircleRadius;
private float mMaxCircleRadius;
private final Paint mCirclePaint;
private int mAlphaValue;
public BreathView(Context context) {
this(context, null);
}
public BreathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BreathView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BreathView);
mCenterCircleRadius = a.getDimension(R.styleable.BreathView_centerCircleRadius, 5f);
mMaxCircleRadius = a.getDimension(R.styleable.BreathView_maxCircleRadius, 10f);
if (mCenterCircleRadius >= mMaxCircleRadius) {
mMaxCircleRadius = mCenterCircleRadius * 2;
}
int circleColor = a.getColor(R.styleable.BreathView_circleColor, Color.GREEN);
a.recycle();
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setColor(circleColor);
ValueAnimator circleAlphaValueAnimator = ValueAnimator.ofInt(0, 255);
circleAlphaValueAnimator.setDuration(BREATH_TIME);
circleAlphaValueAnimator.setRepeatCount(Animation.INFINITE);
circleAlphaValueAnimator.setRepeatMode(ValueAnimator.REVERSE);
circleAlphaValueAnimator.setInterpolator(new LinearInterpolator());
circleAlphaValueAnimator.addUpdateListener(this);
circleAlphaValueAnimator.start();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(widthMeasureSpec);
if(widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
width = (int) Math.max(width, mMaxCircleRadius * 2);
}
if(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
height = (int) Math.max(width, mMaxCircleRadius * 2);
}
setMeasuredDimension(width, height);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
float centerX = getWidth() / 2.0f;
float centerY = getHeight() / 2.0f;
mCirclePaint.setAlpha(255);
canvas.drawCircle(centerX, centerY, mCenterCircleRadius, mCirclePaint);
mCirclePaint.setAlpha(mAlphaValue);
canvas.drawCircle(centerX, centerY, mMaxCircleRadius, mCirclePaint);
}
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAlphaValue = (int) valueAnimator.getAnimatedValue();
invalidate();
}
}
The attribute definition attrs.xml of the custom View is as follows:
<resources>
<declare-styleable name="BreathView">
<attr name="centerCircleRadius" format="dimension"/>
<attr name="circleColor" format="color"/>
<attr name="maxCircleRadius" format="dimension"/>
</declare-styleable>
</resources>
Among them, the size of the middle circle is defined by centerCircleRadius
the property , and the middle circle is not displayed when it is 0; maxCircleRadius
the maximum display radius of the circle is defined by the property, circleColor
and the property is the color of the circle.
Defined in the interface as follows:
<com.example.customui.BreathView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:centerCircleRadius="3dp"
app:maxCircleRadius="8dp"
app:circleColor="@android:color/holo_red_light" />
Application Scenario
It can be passed, setting red can realize the fault alarm effect, and setting green can represent the normal state. Different colors can be defined to achieve colorful breathing light effects, such as power reminder, device failure reminder, device switch status reminder and other effects.