/* * This is the source code of Telegram for Android v. 5.x.x. * It is licensed under GNU GPL v. 2 or later. * You should have received a copy of the license in this archive (see LICENSE). * * Copyright Nikolai Kudashov, 2013-2018. */ package org.telegram.ui.Components; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; import android.util.StateSet; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.Keep; import org.telegram.messenger.AndroidUtilities; import org.telegram.ui.ActionBar.Theme; public class Switch extends View { private RectF rectF; private float progress; private ObjectAnimator checkAnimator; private ObjectAnimator iconAnimator; private boolean attachedToWindow; private boolean isChecked; private Paint paint; private Paint paint2; private int drawIconType; private float iconProgress = 1.0f; private OnCheckedChangeListener onCheckedChangeListener; private String trackColorKey = Theme.key_switch2Track; private String trackCheckedColorKey = Theme.key_switch2TrackChecked; private String thumbColorKey = Theme.key_windowBackgroundWhite; private String thumbCheckedColorKey = Theme.key_windowBackgroundWhite; private Drawable iconDrawable; private int lastIconColor; private boolean drawRipple; private RippleDrawable rippleDrawable; private Paint ripplePaint; private int[] pressedState = new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}; private int colorSet; private boolean bitmapsCreated; private Bitmap[] overlayBitmap; private Canvas[] overlayCanvas; private Bitmap overlayMaskBitmap; private Canvas overlayMaskCanvas; private float overlayCx; private float overlayCy; private float overlayRad; private Paint overlayEraserPaint; private Paint overlayMaskPaint; private Theme.ResourcesProvider resourcesProvider; private int overrideColorProgress; public interface OnCheckedChangeListener { void onCheckedChanged(Switch view, boolean isChecked); } public Switch(Context context) { this(context, null); } public Switch(Context context, Theme.ResourcesProvider resourcesProvider) { super(context); this.resourcesProvider = resourcesProvider; rectF = new RectF(); paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint2 = new Paint(Paint.ANTI_ALIAS_FLAG); paint2.setStyle(Paint.Style.STROKE); paint2.setStrokeCap(Paint.Cap.ROUND); paint2.setStrokeWidth(AndroidUtilities.dp(2)); setHapticFeedbackEnabled(true); } @Keep public void setProgress(float value) { if (progress == value) { return; } progress = value; invalidate(); } @Keep public float getProgress() { return progress; } @Keep public void setIconProgress(float value) { if (iconProgress == value) { return; } iconProgress = value; invalidate(); } @Keep public float getIconProgress() { return iconProgress; } private void cancelCheckAnimator() { if (checkAnimator != null) { checkAnimator.cancel(); checkAnimator = null; } } private void cancelIconAnimator() { if (iconAnimator != null) { iconAnimator.cancel(); iconAnimator = null; } } public void setDrawIconType(int type) { drawIconType = type; } public void setDrawRipple(boolean value) { if (Build.VERSION.SDK_INT < 21 || value == drawRipple) { return; } drawRipple = value; if (rippleDrawable == null) { ripplePaint = new Paint(Paint.ANTI_ALIAS_FLAG); ripplePaint.setColor(0xffffffff); Drawable maskDrawable; if (Build.VERSION.SDK_INT >= 23) { maskDrawable = null; } else { maskDrawable = new Drawable() { @Override public void draw(Canvas canvas) { android.graphics.Rect bounds = getBounds(); canvas.drawCircle(bounds.centerX(), bounds.centerY(), AndroidUtilities.dp(18), ripplePaint); } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } }; } ColorStateList colorStateList = new ColorStateList( new int[][]{StateSet.WILD_CARD}, new int[]{0} ); rippleDrawable = new RippleDrawable(colorStateList, null, maskDrawable); if (Build.VERSION.SDK_INT >= 23) { rippleDrawable.setRadius(AndroidUtilities.dp(18)); } rippleDrawable.setCallback(this); } if (isChecked && colorSet != 2 || !isChecked && colorSet != 1) { int color = isChecked ? Theme.getColor(Theme.key_switchTrackBlueSelectorChecked, resourcesProvider) : Theme.getColor(Theme.key_switchTrackBlueSelector, resourcesProvider); /*if (Build.VERSION.SDK_INT < 28) { color = Color.argb(Color.alpha(color) * 2, Color.red(color), Color.green(color), Color.blue(color)); }*/ ColorStateList colorStateList = new ColorStateList( new int[][]{StateSet.WILD_CARD}, new int[]{color} ); rippleDrawable.setColor(colorStateList); colorSet = isChecked ? 2 : 1; } if (Build.VERSION.SDK_INT >= 28 && value) { rippleDrawable.setHotspot(isChecked ? 0 : AndroidUtilities.dp(100), AndroidUtilities.dp(18)); } rippleDrawable.setState(value ? pressedState : StateSet.NOTHING); invalidate(); } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || rippleDrawable != null && who == rippleDrawable; } public void setColors(String track, String trackChecked, String thumb, String thumbChecked) { trackColorKey = track; trackCheckedColorKey = trackChecked; thumbColorKey = thumb; thumbCheckedColorKey = thumbChecked; } private void animateToCheckedState(boolean newCheckedState) { checkAnimator = ObjectAnimator.ofFloat(this, "progress", newCheckedState ? 1 : 0); checkAnimator.setDuration(semHaptics ? 150 : 250); checkAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { checkAnimator = null; } }); checkAnimator.start(); } private void animateIcon(boolean newCheckedState) { iconAnimator = ObjectAnimator.ofFloat(this, "iconProgress", newCheckedState ? 1 : 0); iconAnimator.setDuration(semHaptics ? 150 : 250); iconAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { iconAnimator = null; } }); iconAnimator.start(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); attachedToWindow = true; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); attachedToWindow = false; } public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { onCheckedChangeListener = listener; } public void setChecked(boolean checked, boolean animated) { setChecked(checked, drawIconType, animated); } public void setChecked(boolean checked, int iconType, boolean animated) { if (checked != isChecked) { isChecked = checked; if (attachedToWindow && animated) { vibrateChecked(checked); animateToCheckedState(checked); } else { cancelCheckAnimator(); setProgress(checked ? 1.0f : 0.0f); } if (onCheckedChangeListener != null) { onCheckedChangeListener.onCheckedChanged(this, checked); } } if (drawIconType != iconType) { drawIconType = iconType; if (attachedToWindow && animated) { animateIcon(iconType == 0); } else { cancelIconAnimator(); setIconProgress(iconType == 0 ? 1.0f : 0.0f); } } } public void setIcon(int icon) { if (icon != 0) { iconDrawable = getResources().getDrawable(icon).mutate(); if (iconDrawable != null) { iconDrawable.setColorFilter(new PorterDuffColorFilter(lastIconColor = Theme.getColor(isChecked ? trackCheckedColorKey : trackColorKey, resourcesProvider), PorterDuff.Mode.MULTIPLY)); } } else { iconDrawable = null; } } public boolean hasIcon() { return iconDrawable != null; } public boolean isChecked() { return isChecked; } public void setOverrideColor(int override) { if (overrideColorProgress == override) { return; } if (overlayBitmap == null) { try { overlayBitmap = new Bitmap[2]; overlayCanvas = new Canvas[2]; for (int a = 0; a < 2; a++) { overlayBitmap[a] = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); overlayCanvas[a] = new Canvas(overlayBitmap[a]); } overlayMaskBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); overlayMaskCanvas = new Canvas(overlayMaskBitmap); overlayEraserPaint = new Paint(Paint.ANTI_ALIAS_FLAG); overlayEraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); overlayMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); overlayMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); bitmapsCreated = true; } catch (Throwable e) { return; } } if (!bitmapsCreated) { return; } overrideColorProgress = override; overlayCx = 0; overlayCy = 0; overlayRad = 0; invalidate(); } public void setOverrideColorProgress(float cx, float cy, float rad) { overlayCx = cx; overlayCy = cy; overlayRad = rad; invalidate(); } @Override protected void onDraw(Canvas canvas) { if (getVisibility() != VISIBLE) { return; } int width = AndroidUtilities.dp(31); int thumb = AndroidUtilities.dp(20); int x = (getMeasuredWidth() - width) / 2; float y = (getMeasuredHeight() - AndroidUtilities.dpf2(14)) / 2; int tx = x + AndroidUtilities.dp(7) + (int) (AndroidUtilities.dp(17) * progress); int ty = getMeasuredHeight() / 2; int color1; int color2; float colorProgress; int r1; int r2; int g1; int g2; int b1; int b2; int a1; int a2; int red; int green; int blue; int alpha; int color; for (int a = 0; a < 2; a++) { if (a == 1 && overrideColorProgress == 0) { continue; } Canvas canvasToDraw = a == 0 ? canvas : overlayCanvas[0]; if (a == 1) { overlayBitmap[0].eraseColor(0); paint.setColor(0xff000000); overlayMaskCanvas.drawRect(0, 0, overlayMaskBitmap.getWidth(), overlayMaskBitmap.getHeight(), paint); overlayMaskCanvas.drawCircle(overlayCx - getX(), overlayCy - getY(), overlayRad, overlayEraserPaint); } if (overrideColorProgress == 1) { colorProgress = a == 0 ? 0 : 1; } else if (overrideColorProgress == 2) { colorProgress = a == 0 ? 1 : 0; } else { colorProgress = progress; } color1 = Theme.getColor(trackColorKey, resourcesProvider); color2 = Theme.getColor(trackCheckedColorKey, resourcesProvider); if (a == 0 && iconDrawable != null && lastIconColor != (isChecked ? color2 : color1)) { iconDrawable.setColorFilter(new PorterDuffColorFilter(lastIconColor = (isChecked ? color2 : color1), PorterDuff.Mode.MULTIPLY)); } r1 = Color.red(color1); r2 = Color.red(color2); g1 = Color.green(color1); g2 = Color.green(color2); b1 = Color.blue(color1); b2 = Color.blue(color2); a1 = Color.alpha(color1); a2 = Color.alpha(color2); red = (int) (r1 + (r2 - r1) * colorProgress); green = (int) (g1 + (g2 - g1) * colorProgress); blue = (int) (b1 + (b2 - b1) * colorProgress); alpha = (int) (a1 + (a2 - a1) * colorProgress); color = ((alpha & 0xff) << 24) | ((red & 0xff) << 16) | ((green & 0xff) << 8) | (blue & 0xff); paint.setColor(color); paint2.setColor(color); rectF.set(x, y, x + width, y + AndroidUtilities.dpf2(14)); canvasToDraw.drawRoundRect(rectF, AndroidUtilities.dpf2(7), AndroidUtilities.dpf2(7), paint); canvasToDraw.drawCircle(tx, ty, AndroidUtilities.dpf2(10), paint); if (a == 0 && rippleDrawable != null) { rippleDrawable.setBounds(tx - AndroidUtilities.dp(18), ty - AndroidUtilities.dp(18), tx + AndroidUtilities.dp(18), ty + AndroidUtilities.dp(18)); rippleDrawable.draw(canvasToDraw); } else if (a == 1) { canvasToDraw.drawBitmap(overlayMaskBitmap, 0, 0, overlayMaskPaint); } } if (overrideColorProgress != 0) { canvas.drawBitmap(overlayBitmap[0], 0, 0, null); } for (int a = 0; a < 2; a++) { if (a == 1 && overrideColorProgress == 0) { continue; } Canvas canvasToDraw = a == 0 ? canvas : overlayCanvas[1]; if (a == 1) { overlayBitmap[1].eraseColor(0); } if (overrideColorProgress == 1) { colorProgress = a == 0 ? 0 : 1; } else if (overrideColorProgress == 2) { colorProgress = a == 0 ? 1 : 0; } else { colorProgress = progress; } color1 = Theme.getColor(thumbColorKey, resourcesProvider); color2 = Theme.getColor(thumbCheckedColorKey, resourcesProvider); r1 = Color.red(color1); r2 = Color.red(color2); g1 = Color.green(color1); g2 = Color.green(color2); b1 = Color.blue(color1); b2 = Color.blue(color2); a1 = Color.alpha(color1); a2 = Color.alpha(color2); red = (int) (r1 + (r2 - r1) * colorProgress); green = (int) (g1 + (g2 - g1) * colorProgress); blue = (int) (b1 + (b2 - b1) * colorProgress); alpha = (int) (a1 + (a2 - a1) * colorProgress); paint.setColor(((alpha & 0xff) << 24) | ((red & 0xff) << 16) | ((green & 0xff) << 8) | (blue & 0xff)); canvasToDraw.drawCircle(tx, ty, AndroidUtilities.dp(8), paint); if (a == 0) { if (iconDrawable != null) { iconDrawable.setBounds(tx - iconDrawable.getIntrinsicWidth() / 2, ty - iconDrawable.getIntrinsicHeight() / 2, tx + iconDrawable.getIntrinsicWidth() / 2, ty + iconDrawable.getIntrinsicHeight() / 2); iconDrawable.draw(canvasToDraw); } else if (drawIconType == 1) { tx -= AndroidUtilities.dp(10.8f) - AndroidUtilities.dp(1.3f) * progress; ty -= AndroidUtilities.dp(8.5f) - AndroidUtilities.dp(0.5f) * progress; int startX2 = (int) AndroidUtilities.dpf2(4.6f) + tx; int startY2 = (int) (AndroidUtilities.dpf2(9.5f) + ty); int endX2 = startX2 + AndroidUtilities.dp(2); int endY2 = startY2 + AndroidUtilities.dp(2); int startX = (int) AndroidUtilities.dpf2(7.5f) + tx; int startY = (int) AndroidUtilities.dpf2(5.4f) + ty; int endX = startX + AndroidUtilities.dp(7); int endY = startY + AndroidUtilities.dp(7); startX = (int) (startX + (startX2 - startX) * progress); startY = (int) (startY + (startY2 - startY) * progress); endX = (int) (endX + (endX2 - endX) * progress); endY = (int) (endY + (endY2 - endY) * progress); canvasToDraw.drawLine(startX, startY, endX, endY, paint2); startX = (int) AndroidUtilities.dpf2(7.5f) + tx; startY = (int) AndroidUtilities.dpf2(12.5f) + ty; endX = startX + AndroidUtilities.dp(7); endY = startY - AndroidUtilities.dp(7); canvasToDraw.drawLine(startX, startY, endX, endY, paint2); } else if (drawIconType == 2 || iconAnimator != null) { paint2.setAlpha((int) (255 * (1.0f - iconProgress))); canvasToDraw.drawLine(tx, ty, tx, ty - AndroidUtilities.dp(5), paint2); canvasToDraw.save(); canvasToDraw.rotate(-90 * iconProgress, tx, ty); canvasToDraw.drawLine(tx, ty, tx + AndroidUtilities.dp(4), ty, paint2); canvasToDraw.restore(); } } if (a == 1) { canvasToDraw.drawBitmap(overlayMaskBitmap, 0, 0, overlayMaskPaint); } } if (overrideColorProgress != 0) { canvas.drawBitmap(overlayBitmap[1], 0, 0, null); } } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName("android.widget.Switch"); info.setCheckable(true); info.setChecked(isChecked); //info.setContentDescription(isChecked ? LocaleController.getString("NotificationsOn", R.string.NotificationsOn) : LocaleController.getString("NotificationsOff", R.string.NotificationsOff)); } private boolean semHaptics = false; private void vibrateChecked(boolean toCheck) { try { if (isHapticFeedbackEnabled() && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { Vibrator vibrator = AndroidUtilities.getVibrator(); VibrationEffect vibrationEffect = VibrationEffect.createWaveform(new long[]{75,10,5,10}, new int[] {5,20,110,20}, -1); vibrator.cancel(); vibrator.vibrate(vibrationEffect); semHaptics = true; } } catch (Exception ignore) {} } }