/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.android.settingslib.graph; import android.animation.ArgbEvaluator; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Path.Direction; import android.graphics.Path.FillType; import android.graphics.Path.Op; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.LayoutDirection; import com.android.settingslib.R; import com.android.settingslib.Utils; public class SignalDrawable extends Drawable { private static final String TAG = "SignalDrawable"; private static final int NUM_DOTS = 3; private static final float VIEWPORT = 24f; private static final float PAD = 2f / VIEWPORT; private static final float CUT_OUT = 7.9f / VIEWPORT; private static final float DOT_SIZE = 3f / VIEWPORT; private static final float DOT_PADDING = 1f / VIEWPORT; private static final float DOT_CUT_WIDTH = (DOT_SIZE * 3) + (DOT_PADDING * 5); private static final float DOT_CUT_HEIGHT = (DOT_SIZE * 1) + (DOT_PADDING * 1); private static final float[] FIT = {2.26f, -3.02f, 1.76f}; // All of these are masks to push all of the drawable state into one int for easy callbacks // and flow through sysui. private static final int LEVEL_MASK = 0xff; private static final int NUM_LEVEL_SHIFT = 8; private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT; private static final int STATE_SHIFT = 16; private static final int STATE_MASK = 0xff << STATE_SHIFT; private static final int STATE_NONE = 0; private static final int STATE_EMPTY = 1; private static final int STATE_CUT = 2; private static final int STATE_CARRIER_CHANGE = 3; private static final int STATE_AIRPLANE = 4; private static final long DOT_DELAY = 1000; private static float[][] X_PATH = new float[][]{ {21.9f / VIEWPORT, 17.0f / VIEWPORT}, {-1.1f / VIEWPORT, -1.1f / VIEWPORT}, {-1.9f / VIEWPORT, 1.9f / VIEWPORT}, {-1.9f / VIEWPORT, -1.9f / VIEWPORT}, {-1.1f / VIEWPORT, 1.1f / VIEWPORT}, {1.9f / VIEWPORT, 1.9f / VIEWPORT}, {-1.9f / VIEWPORT, 1.9f / VIEWPORT}, {1.1f / VIEWPORT, 1.1f / VIEWPORT}, {1.9f / VIEWPORT, -1.9f / VIEWPORT}, {1.9f / VIEWPORT, 1.9f / VIEWPORT}, {1.1f / VIEWPORT, -1.1f / VIEWPORT}, {-1.9f / VIEWPORT, -1.9f / VIEWPORT}, }; // Rounded corners are achieved by arcing a circle of radius `R` from its tangent points along // the curve (curve ≡ triangle). On the top and left corners of the triangle, the tangents are // as follows: // 1) Along the straight lines (y = 0 and x = width): // Ps = circleOffset + R // 2) Along the diagonal line (y = x): // Pd = √((Ps^2) / 2) // or (remember: sin(π/4) ≈ 0.7071) // Pd = (circleOffset + R - 0.7071, height - R - 0.7071) // Where Pd is the (x,y) coords of the point that intersects the circle at the bottom // left of the triangle private static final float RADIUS_RATIO = 0.75f / 17f; private static final float DIAG_OFFSET_MULTIPLIER = 0.707107f; // How far the circle defining the corners is inset from the edges private final float mAppliedCornerInset; private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f); private static final float CUT_WIDTH_DP = 1f / 12f; // Where the top and left points of the triangle would be if not for rounding private final PointF mVirtualTop = new PointF(); private final PointF mVirtualLeft = new PointF(); private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final int mDarkModeBackgroundColor; private final int mDarkModeFillColor; private final int mLightModeBackgroundColor; private final int mLightModeFillColor; private final Path mFullPath = new Path(); private final Path mForegroundPath = new Path(); private final Path mXPath = new Path(); // Cut out when STATE_EMPTY private final Path mCutPath = new Path(); // Draws the slash when in airplane mode private final SlashArtist mSlash = new SlashArtist(); private final Handler mHandler; private float mOldDarkIntensity = -1; private float mNumLevels = 1; private int mIntrinsicSize; private int mLevel; private int mState; private boolean mVisible; private boolean mAnimating; private int mCurrentDot; public SignalDrawable(Context context) { mDarkModeBackgroundColor = Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background); mDarkModeFillColor = Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill); mLightModeBackgroundColor = Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background); mLightModeFillColor = Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill); mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size); mHandler = new Handler(); setDarkIntensity(0); mAppliedCornerInset = context.getResources() .getDimensionPixelSize(R.dimen.stat_sys_mobile_signal_circle_inset); } public void setIntrinsicSize(int size) { mIntrinsicSize = size; } @Override public int getIntrinsicWidth() { return mIntrinsicSize; } @Override public int getIntrinsicHeight() { return mIntrinsicSize; } public void setNumLevels(int levels) { if (levels == mNumLevels) return; mNumLevels = levels; invalidateSelf(); } private void setSignalState(int state) { if (state == mState) return; mState = state; updateAnimation(); invalidateSelf(); } private void updateAnimation() { boolean shouldAnimate = (mState == STATE_CARRIER_CHANGE) && mVisible; if (shouldAnimate == mAnimating) return; mAnimating = shouldAnimate; if (shouldAnimate) { mChangeDot.run(); } else { mHandler.removeCallbacks(mChangeDot); } } @Override protected boolean onLevelChange(int state) { setNumLevels(getNumLevels(state)); setSignalState(getState(state)); int level = getLevel(state); if (level != mLevel) { mLevel = level; invalidateSelf(); } return true; } public void setColors(int background, int foreground) { mPaint.setColor(background); mForegroundPaint.setColor(foreground); } public void setDarkIntensity(float darkIntensity) { if (darkIntensity == mOldDarkIntensity) { return; } mPaint.setColor(getBackgroundColor(darkIntensity)); mForegroundPaint.setColor(getFillColor(darkIntensity)); mOldDarkIntensity = darkIntensity; invalidateSelf(); } private int getFillColor(float darkIntensity) { return getColorForDarkIntensity( darkIntensity, mLightModeFillColor, mDarkModeFillColor); } private int getBackgroundColor(float darkIntensity) { return getColorForDarkIntensity( darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor); } private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) { return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); invalidateSelf(); } @Override public void draw(@NonNull Canvas canvas) { final float width = getBounds().width(); final float height = getBounds().height(); boolean isRtl = getLayoutDirection() == LayoutDirection.RTL; if (isRtl) { canvas.save(); // Mirror the drawable canvas.translate(width, 0); canvas.scale(-1.0f, 1.0f); } mFullPath.reset(); mFullPath.setFillType(FillType.WINDING); final float padding = Math.round(PAD * width); final float cornerRadius = RADIUS_RATIO * height; // Offset from circle where the hypotenuse meets the circle final float diagOffset = DIAG_OFFSET_MULTIPLIER * cornerRadius; // 1 - Bottom right, above corner mFullPath.moveTo(width - padding, height - padding - cornerRadius); // 2 - Line to top right, below corner mFullPath.lineTo(width - padding, padding + cornerRadius + mAppliedCornerInset); // 3 - Arc to top right, on hypotenuse mFullPath.arcTo( width - padding - (2 * cornerRadius), padding + mAppliedCornerInset, width - padding, padding + mAppliedCornerInset + (2 * cornerRadius), 0.f, -135.f, false ); // 4 - Line to bottom left, on hypotenuse mFullPath.lineTo(padding + mAppliedCornerInset + cornerRadius - diagOffset, height - padding - cornerRadius - diagOffset); // 5 - Arc to bottom left, on leg mFullPath.arcTo( padding + mAppliedCornerInset, height - padding - (2 * cornerRadius), padding + mAppliedCornerInset + ( 2 * cornerRadius), height - padding, -135.f, -135.f, false ); // 6 - Line to bottom rght, before corner mFullPath.lineTo(width - padding - cornerRadius, height - padding); // 7 - Arc to beginning (bottom right, above corner) mFullPath.arcTo( width - padding - (2 * cornerRadius), height - padding - (2 * cornerRadius), width - padding, height - padding, 90.f, -90.f, false ); if (mState == STATE_CARRIER_CHANGE) { float cutWidth = (DOT_CUT_WIDTH * width); float cutHeight = (DOT_CUT_HEIGHT * width); float dotSize = (DOT_SIZE * height); float dotPadding = (DOT_PADDING * height); mFullPath.moveTo(width - padding, height - padding); mFullPath.rLineTo(-cutWidth, 0); mFullPath.rLineTo(0, -cutHeight); mFullPath.rLineTo(cutWidth, 0); mFullPath.rLineTo(0, cutHeight); float dotSpacing = dotPadding * 2 + dotSize; float x = width - padding - dotSize; float y = height - padding - dotSize; mForegroundPath.reset(); drawDot(mFullPath, mForegroundPath, x, y, dotSize, 2); drawDot(mFullPath, mForegroundPath, x - dotSpacing, y, dotSize, 1); drawDot(mFullPath, mForegroundPath, x - dotSpacing * 2, y, dotSize, 0); } else if (mState == STATE_CUT) { float cut = (CUT_OUT * width); mFullPath.moveTo(width - padding, height - padding); mFullPath.rLineTo(-cut, 0); mFullPath.rLineTo(0, -cut); mFullPath.rLineTo(cut, 0); mFullPath.rLineTo(0, cut); } if (mState == STATE_EMPTY) { // Where the corners would be if this were a real triangle mVirtualTop.set( width - padding, (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius)); mVirtualLeft.set( (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius), height - padding); final float cutWidth = CUT_WIDTH_DP * height; final float cutDiagInset = cutWidth * INV_TAN; // Cut out a smaller triangle from the center of mFullPath mCutPath.reset(); mCutPath.setFillType(FillType.WINDING); mCutPath.moveTo(width - padding - cutWidth, height - padding - cutWidth); mCutPath.lineTo(width - padding - cutWidth, mVirtualTop.y + cutDiagInset); mCutPath.lineTo(mVirtualLeft.x + cutDiagInset, height - padding - cutWidth); mCutPath.lineTo(width - padding - cutWidth, height - padding - cutWidth); // Draw empty state as only background mForegroundPath.reset(); mFullPath.op(mCutPath, Path.Op.DIFFERENCE); } else if (mState == STATE_AIRPLANE) { // Airplane mode is slashed, fully drawn background mForegroundPath.reset(); mSlash.draw((int) height, (int) width, canvas, mPaint); } else if (mState != STATE_CARRIER_CHANGE) { mForegroundPath.reset(); int sigWidth = Math.round(calcFit(mLevel / (mNumLevels - 1)) * (width - 2 * padding)); mForegroundPath.addRect(padding, padding, padding + sigWidth, height - padding, Direction.CW); mForegroundPath.op(mFullPath, Op.INTERSECT); } canvas.drawPath(mFullPath, mPaint); canvas.drawPath(mForegroundPath, mForegroundPaint); if (mState == STATE_CUT) { mXPath.reset(); mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height); for (int i = 1; i < X_PATH.length; i++) { mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height); } canvas.drawPath(mXPath, mForegroundPaint); } if (isRtl) { canvas.restore(); } } private void drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize, int i) { Path p = (i == mCurrentDot) ? foregroundPath : fullPath; p.addRect(x, y, x + dotSize, y + dotSize, Direction.CW); } // This is a fit line based on previous values of provided in assets, but if // you look at the a plot of this actual fit, it makes a lot of sense, what it does // is compress the areas that are very visually easy to see changes (the middle sections) // and spread out the sections that are hard to see (each end of the icon). // The current fit is cubic, but pretty easy to change the way the code is written (just add // terms to the end of FIT). private float calcFit(float v) { float ret = 0; float t = v; for (int i = 0; i < FIT.length; i++) { ret += FIT[i] * t; t *= v; } return ret; } @Override public int getAlpha() { return mPaint.getAlpha(); } @Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { mPaint.setAlpha(alpha); mForegroundPaint.setAlpha(alpha); } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { mPaint.setColorFilter(colorFilter); mForegroundPaint.setColorFilter(colorFilter); } @Override public int getOpacity() { return 255; } @Override public boolean setVisible(boolean visible, boolean restart) { mVisible = visible; updateAnimation(); return super.setVisible(visible, restart); } private final Runnable mChangeDot = new Runnable() { @Override public void run() { if (++mCurrentDot == NUM_DOTS) { mCurrentDot = 0; } invalidateSelf(); mHandler.postDelayed(mChangeDot, DOT_DELAY); } }; public static int getLevel(int fullState) { return fullState & LEVEL_MASK; } public static int getState(int fullState) { return (fullState & STATE_MASK) >> STATE_SHIFT; } public static int getNumLevels(int fullState) { return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT; } public static int getState(int level, int numLevels, boolean cutOut) { return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT) | level; } public static int getCarrierChangeState(int numLevels) { return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT); } public static int getEmptyState(int numLevels) { return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT); } public static int getAirplaneModeState(int numLevels) { return (STATE_AIRPLANE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT); } private final class SlashArtist { private static final float CORNER_RADIUS = 1f; // These values are derived in un-rotated (vertical) orientation private static final float SLASH_WIDTH = 1.8384776f; private static final float SLASH_HEIGHT = 22f; private static final float CENTER_X = 10.65f; private static final float CENTER_Y = 15.869239f; private static final float SCALE = 24f; // Bottom is derived during animation private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE; private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE; private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE; private static final float BOTTOM = (CENTER_Y + (SLASH_HEIGHT / 2)) / SCALE; // Draw the slash washington-monument style; rotate to no-u-turn style private static final float ROTATION = -45f; private final Path mPath = new Path(); private final RectF mSlashRect = new RectF(); void draw(int height, int width, @NonNull Canvas canvas, Paint paint) { Matrix m = new Matrix(); final float radius = scale(CORNER_RADIUS, width); updateRect( scale(LEFT, width), scale(TOP, height), scale(RIGHT, width), scale(BOTTOM, height)); mPath.reset(); // Draw the slash vertically mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW); m.setRotate(ROTATION, width / 2, height / 2); mPath.transform(m); canvas.drawPath(mPath, paint); // Rotate back to vertical, and draw the cut-out rect next to this one m.setRotate(-ROTATION, width / 2, height / 2); mPath.transform(m); m.setTranslate(mSlashRect.width(), 0); mPath.transform(m); mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW); m.setRotate(ROTATION, width / 2, height / 2); mPath.transform(m); canvas.clipOutPath(mPath); } void updateRect(float left, float top, float right, float bottom) { mSlashRect.left = left; mSlashRect.top = top; mSlashRect.right = right; mSlashRect.bottom = bottom; } private float scale(float frac, int width) { return frac * width; } } }