| /* |
| * 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.content.res.ColorStateList; |
| 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.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.graphics.Rect; |
| import android.graphics.drawable.DrawableWrapper; |
| import android.os.Handler; |
| import android.telephony.CellSignalStrength; |
| import android.util.LayoutDirection; |
| import android.util.PathParser; |
| |
| import com.android.settingslib.R; |
| import com.android.settingslib.Utils; |
| |
| /** |
| * Drawable displaying a mobile cell signal indicator. |
| */ |
| public class SignalDrawable extends DrawableWrapper { |
| |
| 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 DOT_SIZE = 3f / VIEWPORT; |
| private static final float DOT_PADDING = 1.5f / VIEWPORT; |
| |
| // 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_CUT = 2; |
| private static final int STATE_CARRIER_CHANGE = 3; |
| |
| private static final long DOT_DELAY = 1000; |
| |
| private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint mTransparentPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final int mDarkModeFillColor; |
| private final int mLightModeFillColor; |
| private final Path mCutoutPath = new Path(); |
| private final Path mForegroundPath = new Path(); |
| private final Path mXPath = new Path(); |
| private final Matrix mXScaleMatrix = new Matrix(); |
| private final Path mScaledXPath = new Path(); |
| private final Handler mHandler; |
| private final float mCutoutWidthFraction; |
| private final float mCutoutHeightFraction; |
| private float mDarkIntensity = -1; |
| private final int mIntrinsicSize; |
| private boolean mAnimating; |
| private int mCurrentDot; |
| |
| public SignalDrawable(Context context) { |
| super(context.getDrawable(com.android.internal.R.drawable.ic_signal_cellular)); |
| final String xPathString = context.getString( |
| com.android.internal.R.string.config_signalXPath); |
| mXPath.set(PathParser.createPathFromPathData(xPathString)); |
| updateScaledXPath(); |
| mCutoutWidthFraction = context.getResources().getFloat( |
| com.android.internal.R.dimen.config_signalCutoutWidthFraction); |
| mCutoutHeightFraction = context.getResources().getFloat( |
| com.android.internal.R.dimen.config_signalCutoutHeightFraction); |
| mDarkModeFillColor = Utils.getColorStateListDefaultColor(context, |
| R.color.dark_mode_icon_color_single_tone); |
| mLightModeFillColor = Utils.getColorStateListDefaultColor(context, |
| R.color.light_mode_icon_color_single_tone); |
| mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size); |
| mTransparentPaint.setColor(context.getColor(android.R.color.transparent)); |
| mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); |
| mHandler = new Handler(); |
| setDarkIntensity(0); |
| } |
| |
| private void updateScaledXPath() { |
| if (getBounds().isEmpty()) { |
| mXScaleMatrix.setScale(1f, 1f); |
| } else { |
| mXScaleMatrix.setScale(getBounds().width() / VIEWPORT, getBounds().height() / VIEWPORT); |
| } |
| mXPath.transform(mXScaleMatrix, mScaledXPath); |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return mIntrinsicSize; |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return mIntrinsicSize; |
| } |
| |
| private void updateAnimation() { |
| boolean shouldAnimate = isInState(STATE_CARRIER_CHANGE) && isVisible(); |
| if (shouldAnimate == mAnimating) return; |
| mAnimating = shouldAnimate; |
| if (shouldAnimate) { |
| mChangeDot.run(); |
| } else { |
| mHandler.removeCallbacks(mChangeDot); |
| } |
| } |
| |
| @Override |
| protected boolean onLevelChange(int packedState) { |
| super.onLevelChange(unpackLevel(packedState)); |
| updateAnimation(); |
| setTintList(ColorStateList.valueOf(mForegroundPaint.getColor())); |
| invalidateSelf(); |
| return true; |
| } |
| |
| private int unpackLevel(int packedState) { |
| int numBins = (packedState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT; |
| int levelOffset = numBins == (CellSignalStrength.getNumSignalStrengthLevels() + 1) ? 10 : 0; |
| int level = (packedState & LEVEL_MASK); |
| return level + levelOffset; |
| } |
| |
| public void setDarkIntensity(float darkIntensity) { |
| if (darkIntensity == mDarkIntensity) { |
| return; |
| } |
| setTintList(ColorStateList.valueOf(getFillColor(darkIntensity))); |
| } |
| |
| @Override |
| public void setTintList(ColorStateList tint) { |
| super.setTintList(tint); |
| int colorForeground = mForegroundPaint.getColor(); |
| mForegroundPaint.setColor(tint.getDefaultColor()); |
| if (colorForeground != mForegroundPaint.getColor()) invalidateSelf(); |
| } |
| |
| private int getFillColor(float darkIntensity) { |
| return getColorForDarkIntensity( |
| darkIntensity, mLightModeFillColor, mDarkModeFillColor); |
| } |
| |
| 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); |
| updateScaledXPath(); |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void draw(@NonNull Canvas canvas) { |
| canvas.saveLayer(null, null); |
| 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); |
| } |
| super.draw(canvas); |
| mCutoutPath.reset(); |
| mCutoutPath.setFillType(FillType.WINDING); |
| |
| final float padding = Math.round(PAD * width); |
| |
| if (isInState(STATE_CARRIER_CHANGE)) { |
| float dotSize = (DOT_SIZE * height); |
| float dotPadding = (DOT_PADDING * height); |
| float dotSpacing = dotPadding + dotSize; |
| float x = width - padding - dotSize; |
| float y = height - padding - dotSize; |
| mForegroundPath.reset(); |
| drawDotAndPadding(x, y, dotPadding, dotSize, 2); |
| drawDotAndPadding(x - dotSpacing, y, dotPadding, dotSize, 1); |
| drawDotAndPadding(x - dotSpacing * 2, y, dotPadding, dotSize, 0); |
| canvas.drawPath(mCutoutPath, mTransparentPaint); |
| canvas.drawPath(mForegroundPath, mForegroundPaint); |
| } else if (isInState(STATE_CUT)) { |
| float cutX = (mCutoutWidthFraction * width / VIEWPORT); |
| float cutY = (mCutoutHeightFraction * height / VIEWPORT); |
| mCutoutPath.moveTo(width, height); |
| mCutoutPath.rLineTo(-cutX, 0); |
| mCutoutPath.rLineTo(0, -cutY); |
| mCutoutPath.rLineTo(cutX, 0); |
| mCutoutPath.rLineTo(0, cutY); |
| canvas.drawPath(mCutoutPath, mTransparentPaint); |
| canvas.drawPath(mScaledXPath, mForegroundPaint); |
| } |
| if (isRtl) { |
| canvas.restore(); |
| } |
| canvas.restore(); |
| } |
| |
| private void drawDotAndPadding(float x, float y, |
| float dotPadding, float dotSize, int i) { |
| if (i == mCurrentDot) { |
| // Draw dot |
| mForegroundPath.addRect(x, y, x + dotSize, y + dotSize, Direction.CW); |
| // Draw dot padding |
| mCutoutPath.addRect(x - dotPadding, y - dotPadding, x + dotSize + dotPadding, |
| y + dotSize + dotPadding, Direction.CW); |
| } |
| } |
| |
| @Override |
| public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { |
| super.setAlpha(alpha); |
| mForegroundPaint.setAlpha(alpha); |
| } |
| |
| @Override |
| public void setColorFilter(@Nullable ColorFilter colorFilter) { |
| super.setColorFilter(colorFilter); |
| mForegroundPaint.setColorFilter(colorFilter); |
| } |
| |
| @Override |
| public boolean setVisible(boolean visible, boolean restart) { |
| boolean changed = super.setVisible(visible, restart); |
| updateAnimation(); |
| return changed; |
| } |
| |
| private final Runnable mChangeDot = new Runnable() { |
| @Override |
| public void run() { |
| if (++mCurrentDot == NUM_DOTS) { |
| mCurrentDot = 0; |
| } |
| invalidateSelf(); |
| mHandler.postDelayed(mChangeDot, DOT_DELAY); |
| } |
| }; |
| |
| /** |
| * Returns whether this drawable is in the specified state. |
| * |
| * @param state must be one of {@link #STATE_CARRIER_CHANGE} or {@link #STATE_CUT} |
| */ |
| private boolean isInState(int state) { |
| return getState(getLevel()) == state; |
| } |
| |
| public static int getState(int fullState) { |
| return (fullState & STATE_MASK) >> STATE_SHIFT; |
| } |
| |
| public static int getState(int level, int numLevels, boolean cutOut) { |
| return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT) |
| | (numLevels << NUM_LEVEL_SHIFT) |
| | level; |
| } |
| |
| /** Returns the state representing empty mobile signal with the given number of levels. */ |
| public static int getEmptyState(int numLevels) { |
| return getState(0, numLevels, true); |
| } |
| |
| /** Returns the state representing carrier change with the given number of levels. */ |
| public static int getCarrierChangeState(int numLevels) { |
| return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT); |
| } |
| } |