看见别人用kotlin写的Switcher,我刚好想了解一下Android上的动画怎么玩,于是自己就慢慢跟着大神们写的代码用Java实现重新实现了,学习过程中,如果有什么需要改进的地方,希望有人可以提出,我会加以改正。

Java源码
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.RectF;
import android.os.Build;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewOutlineProvider;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
public class Switcher extends View {
private int onColor = 0;
private int offColor = 0;
private int iconColor = 0;
private float switcherCornerRadius = 0f;
private int defHeight = 0;
private int defWidth = 0;
//背景
private RectF switcherRect = new RectF(0f, 0f, 0f, 0f);
private Paint switcherPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//图标
private RectF iconRect = new RectF(0f, 0f, 0f, 0f);
private RectF iconClipRect = new RectF(0f, 0f, 0f, 0f);
private Paint iconPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private float switchElevation = 0f;
private float shadowOffset = 0f;
private float iconTranslateX = 0f;
private boolean isChecked = true;
private float iconCollapsedWidth = 0f;
private float iconRadius = 0f;
private float iconClipRadius = 0f;
private float iconHeight = 0f;
private Paint iconClipPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private AnimatorSet animatorSet =new AnimatorSet();
private int currentColor = 0;
private Long COLOR_ANIMATION_DURATION = 300L;
private Bitmap shadow = null;
private float iconProgress = 0f;
private float onClickOffset = 0f;
private Paint shadowPaint =new Paint(Paint.ANTI_ALIAS_FLAG);
private void setOnClickOffset(float onClickOffset){
this.onClickOffset = onClickOffset;
switcherRect.left = onClickOffset + shadowOffset;
switcherRect.top = onClickOffset + shadowOffset / 2;
switcherRect.right = getWidth() - onClickOffset - shadowOffset;
switcherRect.bottom = getHeight() - onClickOffset - shadowOffset - shadowOffset / 2;
if (!isLollipopAndAbove()) generateShadow();
invalidate();
}
private float lerp(float a,float b,float t){
return a + (b - a) * t;
}
private void setIconProgress(float iconProgress){
if(iconProgress!=this.iconProgress){
this.iconProgress = iconProgress;
float iconOffset = lerp(0f, iconRadius - iconCollapsedWidth / 2, iconProgress);
iconRect.left = getWidth() - switcherCornerRadius - iconCollapsedWidth / 2 - iconOffset;
iconRect.right = getWidth() - switcherCornerRadius + iconCollapsedWidth / 2 + iconOffset;
float clipOffset = lerp(0f, iconClipRadius, iconProgress);
iconClipRect.set(
iconRect.centerX() - clipOffset,
iconRect.centerY() - clipOffset,
iconRect.centerX() + clipOffset,
iconRect.centerY() + clipOffset
);
if (!isLollipopAndAbove()) generateShadow();
postInvalidateOnAnimation();
}
}
private boolean isLollipopAndAbove(){
return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP;
}
public Switcher(Context context) {
this(context,null);
}
public Switcher(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public Switcher(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Switcher);
onColor = typedArray.getColor(R.styleable.Switcher_switcher_on_color, 0);
offColor = typedArray.getColor(R.styleable.Switcher_switcher_off_color, 0);
iconColor = typedArray.getColor(R.styleable.Switcher_switcher_icon_color, 0);
switchElevation = typedArray.getDimension(R.styleable.Switcher_elevation, 0f);
defHeight = typedArray.getDimensionPixelOffset(R.styleable.Switcher_switcher_height, 0);
defWidth = typedArray.getDimensionPixelOffset(R.styleable.Switcher_switcher_width, 0);
isChecked = typedArray.getBoolean(R.styleable.Switcher_android_checked, true);
if (isChecked) setCurrentColor(onColor); else setCurrentColor(offColor);
iconPaint.setColor(iconColor);
typedArray.recycle();
if (!isLollipopAndAbove() && switchElevation > 0f) {
shadowPaint.setColorFilter(new PorterDuffColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN));
shadowPaint.setAlpha(51);
setShadowBlurRadius(switchElevation);
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
setChecked(!isChecked);
}
});
}
private float toPx(float value){
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, getContext().getResources().getDisplayMetrics());
}
private void setShadowBlurRadius(float elevation) {
float maxElevation = toPx(24f);
switchElevation = Math.min(25f * (elevation / maxElevation), 25f);
}
private void setChecked(Boolean checked){
this.setChecked(checked,true);
}
private void setChecked(Boolean checked, Boolean withAnimation){
if (this.isChecked != checked) {
this.isChecked = checked;
if (withAnimation && getWidth() != 0) {
animateSwitch();
}else {
animatorSet.cancel();
if (!checked) {
currentColor = offColor;
iconProgress = 1f;
iconTranslateX = -(getWidth() - shadowOffset - switcherCornerRadius * 2);
} else {
currentColor = onColor;
iconProgress = 0f;
iconTranslateX = -shadowOffset;
}
}
}
}
private void animateSwitch(){
animatorSet.cancel();
animatorSet = new AnimatorSet();
float newProgress = 1f;
float iconTranslateA = 0f;
float iconTranslateB = -(getWidth() - shadowOffset - switcherCornerRadius * 2);
if (isChecked) {
iconTranslateA = iconTranslateB;
iconTranslateB = -shadowOffset;
newProgress = 0f;
}
ValueAnimator switcherAnimator = ValueAnimator.ofFloat(iconProgress, newProgress);
switcherAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
setIconProgress((float)valueAnimator.getAnimatedValue());
}
});
ValueAnimator translateAnimator = ValueAnimator.ofFloat(0f, 1f);
final float finalIconTranslateA = iconTranslateA;
final float finalIconTranslateB = iconTranslateB;
translateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float value = (float)valueAnimator.getAnimatedValue();
iconTranslateX = lerp(finalIconTranslateA, finalIconTranslateB, value);
}
});
translateAnimator.addListener(new Animator.AnimatorListener(){
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
setOnClickOffset(0f);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
int toColor = isChecked?onColor:offColor;
iconClipPaint.setColor(toColor);
ValueAnimator colorAnimator =ValueAnimator.ofInt(currentColor, toColor);
colorAnimator.setEvaluator(new ArgbEvaluator());
colorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
setCurrentColor((int) valueAnimator.getAnimatedValue());
}
});
animatorSet.playTogether(switcherAnimator,translateAnimator,colorAnimator);
animatorSet.setDuration(500);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
if(listenner!=null){
listenner.onClick(isChecked);
}
}
@Override
public void onAnimationEnd(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animatorSet.start();
}
private void generateShadow() {
if (switchElevation == 0f) return;
if (!isInEditMode()) {
if (shadow == null) {
shadow = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ALPHA_8);
} else {
shadow.eraseColor(Color.TRANSPARENT);
}
Canvas c = new Canvas(shadow);
c.drawRoundRect(switcherRect, switcherCornerRadius, switcherCornerRadius, shadowPaint);
RenderScript rs = RenderScript.create(getContext());
ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(rs, Element.U8(rs));
Allocation input = Allocation.createFromBitmap(rs, shadow);
Allocation output = Allocation.createTyped(rs, input.getType());
blur.setRadius(switchElevation);
blur.setInput(input);
blur.forEach(output);
output.copyTo(shadow);
input.destroy();
output.destroy();
blur.destroy();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isLollipopAndAbove() && switchElevation > 0f && !isInEditMode()) {
canvas.drawBitmap(shadow, 0f, shadowOffset, null);
}
canvas.drawRoundRect(switcherRect, switcherCornerRadius, switcherCornerRadius, switcherPaint);
canvas.translate(iconTranslateX,0);
canvas.drawRoundRect(iconRect, switcherCornerRadius, switcherCornerRadius, iconPaint);
if (iconClipRect.width() > iconCollapsedWidth)
canvas.drawRoundRect(iconClipRect, iconRadius, iconRadius, iconClipPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
width = defWidth;
height = defHeight;
}
if (!isLollipopAndAbove()) {
width += ((int)switchElevation) * 2;
height += ((int)switchElevation) * 2;
}
setMeasuredDimension(width, height);
}
private void setCurrentColor(int color){
currentColor = color;
switcherPaint.setColor(color);
iconClipPaint.setColor(color);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setOutlineProvider(new SwitchOutline(w, h));
setElevation(switchElevation);
} else {
shadowOffset = switchElevation;
iconTranslateX = -shadowOffset;
}
switcherRect.left = shadowOffset;
switcherRect.top = shadowOffset / 2;
switcherRect.right = getWidth() - shadowOffset;
switcherRect.bottom = getHeight() - shadowOffset - shadowOffset / 2;
switcherCornerRadius = (getHeight() - shadowOffset * 2) / 2f;
iconRadius = switcherCornerRadius * 0.6f;
iconClipRadius = iconRadius / 2.25f;
iconCollapsedWidth = iconRadius - iconClipRadius;
iconHeight = iconRadius * 2f;
iconRect.set(
getWidth() - switcherCornerRadius - iconCollapsedWidth / 2,
((getHeight() - iconHeight) / 2f) - shadowOffset / 2,
getWidth() - switcherCornerRadius + iconCollapsedWidth / 2,
(getHeight() - (getHeight() - iconHeight) / 2f) - shadowOffset / 2
);
if (!isChecked) {
iconRect.left = getWidth() - switcherCornerRadius - iconCollapsedWidth / 2 - (iconRadius - iconCollapsedWidth / 2);
iconRect.right = getWidth() - switcherCornerRadius + iconCollapsedWidth / 2 + (iconRadius - iconCollapsedWidth / 2);
iconClipRect.set(
iconRect.centerX() - iconClipRadius,
iconRect.centerY() - iconClipRadius,
iconRect.centerX() + iconClipRadius,
iconRect.centerY() + iconClipRadius
);
iconTranslateX = -(getWidth() - shadowOffset - switcherCornerRadius * 2);
}
if (!isLollipopAndAbove()) generateShadow();
}
private Listenner listenner = null;
public void setListenner(Listenner listenner) {
this.listenner = listenner;
}
private interface Listenner{
public void onClick(boolean isCheck);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private class SwitchOutline extends ViewOutlineProvider {
private int width,height;
public SwitchOutline(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, width, height, switcherCornerRadius);
}
}
}
attrs:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Switcher">
<attr name="switcher_on_color" format="color"/>
<attr name="switcher_off_color" format="color"/>
<attr name="switcher_icon_color" format="color"/>
<attr name="elevation" format="dimension"/>
<attr name="switcher_height" format="dimension"/>
<attr name="switcher_width" format="dimension"/>
</declare-styleable>
</resources>
调用方法:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.qmuitest.TestView
android:id="@+id/switcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:switcher_height="60dp"
app:switcher_width="120dp"
app:layout_constraintRight_toRightOf="parent"
app:switcher_on_color="@android:color/holo_blue_bright"
app:switcher_off_color="@android:color/holo_red_light"
app:switcher_icon_color="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>

参考:
炫酷!从未见过如此Q弹的Switcher: https://blog.csdn.net/zwluoyuxi/article/details/104824809
网友评论