diff options
author | Jason A. Donenfeld <Jason@zx2c4.com> | 2018-07-09 18:40:41 +0200 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2018-07-09 18:55:55 +0200 |
commit | 3cf6aad083eb0b207142d1c63b3c1253071622d7 (patch) | |
tree | 8d1bdb5cd404b2f5ec1a4ced06953b86c0c6f708 /app/src | |
parent | b997a2581b9a1254584f4dbfdcfe00248bf09a30 (diff) |
QuickTileService: automatically slash the tile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'app/src')
3 files changed, 262 insertions, 33 deletions
diff --git a/app/src/main/java/com/wireguard/android/QuickTileService.java b/app/src/main/java/com/wireguard/android/QuickTileService.java index c93a7c5c..21081034 100644 --- a/app/src/main/java/com/wireguard/android/QuickTileService.java +++ b/app/src/main/java/com/wireguard/android/QuickTileService.java @@ -10,6 +10,10 @@ import android.annotation.TargetApi; import android.content.Intent; import android.databinding.Observable; import android.databinding.Observable.OnPropertyChangedCallback; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Build; import android.service.quicksettings.Tile; @@ -21,6 +25,7 @@ import com.wireguard.android.activity.MainActivity; import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.util.ExceptionLoggers; +import com.wireguard.android.widget.SlashDrawable; import java.util.Objects; @@ -37,6 +42,26 @@ public class QuickTileService extends TileService { private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback(); private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback(); private Tunnel tunnel; + private Icon iconOn; + private Icon iconOff; + + @Override + public void onCreate() { + final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile)); + icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */ + icon.setSlashed(false); + Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + icon.setBounds(0, 0, c.getWidth(), c.getHeight()); + icon.draw(c); + iconOn = Icon.createWithBitmap(b); + icon.setSlashed(true); + b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + c = new Canvas(b); + icon.setBounds(0, 0, c.getWidth(), c.getHeight()); + icon.draw(c); + iconOff = Icon.createWithBitmap(b); + } @Override public void onClick() { @@ -99,10 +124,7 @@ public class QuickTileService extends TileService { return; tile.setLabel(label); if (tile.getState() != state) { - // The icon must be changed every time the state changes, or the shade will not change. - final Integer iconResource = state == Tile.STATE_ACTIVE ? R.drawable.ic_tile - : R.drawable.ic_tile_disabled; - tile.setIcon(Icon.createWithResource(this, iconResource)); + tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff); tile.setState(state); } tile.updateTile(); diff --git a/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java b/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java new file mode 100644 index 00000000..bd2da16b --- /dev/null +++ b/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java @@ -0,0 +1,236 @@ +/* + * Copyright © 2018 The Android Open Source Project + * Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.wireguard.android.widget; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +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.PixelFormat; +import android.graphics.PorterDuff.Mode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.ColorInt; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.FloatProperty; + + +@TargetApi(Build.VERSION_CODES.N) +public class SlashDrawable extends Drawable { + + private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f; + private static final long QS_ANIM_LENGTH = 350; + + private final Path mPath = new Path(); + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + // These values are derived in un-rotated (vertical) orientation + private static final float SLASH_WIDTH = 1.8384776f; + private static final float SLASH_HEIGHT = 28f; + private static final float CENTER_X = 10.65f; + private static final float CENTER_Y = 11.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; + // Draw the slash washington-monument style; rotate to no-u-turn style + private static final float DEFAULT_ROTATION = -45f; + + private Drawable mDrawable; + private final RectF mSlashRect = new RectF(0, 0, 0, 0); + private float mRotation; + private boolean mSlashed; + private Mode mTintMode; + private ColorStateList mTintList; + private boolean mAnimationEnabled = true; + + public SlashDrawable(final Drawable d) { + setDrawable(d); + } + + @Override + public int getIntrinsicHeight() { + return mDrawable != null ? mDrawable.getIntrinsicHeight(): 0; + } + + @Override + public int getIntrinsicWidth() { + return mDrawable != null ? mDrawable.getIntrinsicWidth(): 0; + } + + @Override + protected void onBoundsChange(final Rect bounds) { + super.onBoundsChange(bounds); + mDrawable.setBounds(bounds); + } + + public void setDrawable(final Drawable d) { + mDrawable = d; + mDrawable.setCallback(getCallback()); + mDrawable.setBounds(getBounds()); + if (mTintMode != null) + mDrawable.setTintMode(mTintMode); + if (mTintList != null) + mDrawable.setTintList(mTintList); + invalidateSelf(); + } + + public void setRotation(final float rotation) { + if (mRotation == rotation) + return; + mRotation = rotation; + invalidateSelf(); + } + + public void setAnimationEnabled(final boolean enabled) { + mAnimationEnabled = enabled; + } + + // Animate this value on change + private float mCurrentSlashLength; + private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") { + @Override + public void setValue(final SlashDrawable object, final float value) { + object.mCurrentSlashLength = value; + } + + @Override + public Float get(final SlashDrawable object) { + return object.mCurrentSlashLength; + } + }; + + public void setSlashed(final boolean slashed) { + if (mSlashed == slashed) return; + + mSlashed = slashed; + + final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f; + final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE; + + if (mAnimationEnabled) { + final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end); + anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf()); + anim.setDuration(QS_ANIM_LENGTH); + anim.start(); + } else { + mCurrentSlashLength = end; + invalidateSelf(); + } + } + + @Override + public void draw(@NonNull final Canvas canvas) { + canvas.save(); + final Matrix m = new Matrix(); + final int width = getBounds().width(); + final int height = getBounds().height(); + final float radiusX = scale(CORNER_RADIUS, width); + final float radiusY = scale(CORNER_RADIUS, height); + updateRect( + scale(LEFT, width), + scale(TOP, height), + scale(RIGHT, width), + scale(TOP + mCurrentSlashLength, height) + ); + + mPath.reset(); + // Draw the slash vertically + mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW); + // Rotate -45 + desired rotation + m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2); + mPath.transform(m); + canvas.drawPath(mPath, mPaint); + + // Rotate back to vertical + m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2); + mPath.transform(m); + + // Draw another rect right next to the first, for clipping + m.setTranslate(mSlashRect.width(), 0); + mPath.transform(m); + mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW); + m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2); + mPath.transform(m); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + canvas.clipPath(mPath, Region.Op.DIFFERENCE); + else + canvas.clipOutPath(mPath); + + mDrawable.draw(canvas); + canvas.restore(); + } + + private float scale(final float frac, final int width) { + return frac * width; + } + + private void updateRect(final float left, final float top, final float right, final float bottom) { + mSlashRect.left = left; + mSlashRect.top = top; + mSlashRect.right = right; + mSlashRect.bottom = bottom; + } + + @Override + public void setTint(@ColorInt final int tintColor) { + super.setTint(tintColor); + mDrawable.setTint(tintColor); + mPaint.setColor(tintColor); + } + + @Override + public void setTintList(@Nullable final ColorStateList tint) { + mTintList = tint; + super.setTintList(tint); + setDrawableTintList(tint); + mPaint.setColor(tint == null ? 0 : tint.getDefaultColor()); + invalidateSelf(); + } + + private void setDrawableTintList(@Nullable final ColorStateList tint) { + mDrawable.setTintList(tint); + } + + @Override + public void setTintMode(@NonNull final Mode tintMode) { + mTintMode = tintMode; + super.setTintMode(tintMode); + mDrawable.setTintMode(tintMode); + } + + @Override + public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) { + mDrawable.setAlpha(alpha); + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(@Nullable final ColorFilter colorFilter) { + mDrawable.setColorFilter(colorFilter); + mPaint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} diff --git a/app/src/main/res/drawable/ic_tile_disabled.xml b/app/src/main/res/drawable/ic_tile_disabled.xml deleted file mode 100644 index 3109c39a..00000000 --- a/app/src/main/res/drawable/ic_tile_disabled.xml +++ /dev/null @@ -1,29 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="400dp" - android:height="400dp" - android:viewportHeight="400.0" - android:viewportWidth="400.0"> - <path - android:fillAlpha="1" - android:fillColor="#ffffff" - android:pathData="M197.7,0C191.5,0.1 185.2,0.5 178.7,1.1C191.1,4 202.2,6.6 213.4,9.2C213.3,9.8 213.1,10.5 213,11.2C198.1,13.2 183.9,7.7 169.3,5.7C174.6,8.8 180,11.7 185.5,14.2C191.2,16.7 197,18.8 202.8,21.2C195.4,27.5 188,28.8 178.7,26.7C173.7,25.6 168.3,25 163.1,25.2C157.8,25.5 152.4,26.8 147.5,30C152.7,32.7 157.5,34.8 162.1,37.5C163.9,38.6 166.1,40.4 166.6,42.4C167.8,46.9 168.2,51.8 168.9,56.5C160.4,57.5 145.4,66.1 142.4,71.7C155.5,74.3 169.7,71.2 182.2,79.6C178.1,82.8 168.5,86.6 165,89.3C169.3,90.4 179.4,89.9 183.3,89.6C186.6,89.4 188.2,89.3 189.5,90.4L228,120.6C232.1,123.8 248.4,139.3 252.7,149C256.3,157.3 256.8,164.4 256.8,166.1C256.8,170.7 256.2,177.9 253,186C252.7,186.9 252,188.3 251.2,189.8L280.8,219.4C288.3,207.7 293.4,194.5 296.3,180C299.6,163.2 299.4,146.5 291.7,130.5C285.8,118.3 276.1,109.4 265.8,101.3C255.1,93 243.7,85.4 233.1,77C230.2,74.8 228.3,70.8 226.9,67.3C226.4,65.8 228.2,61.7 229.4,61.5C236,60.3 242.6,59.7 249.3,59.4C256.9,59.2 264.6,59.4 272.3,59.5C274,59.5 276.2,59.3 277.2,60.3C281.2,64.2 284.3,61.6 287,59.1C289.4,56.9 291,54 292.9,51.6C291.7,51.4 289.4,50.9 287.1,50.8C279.4,50.6 271.7,50.7 264,50.5C262.6,50.4 261.3,49 259.9,48.2C261.3,47.6 262.8,46.6 264.2,46.6C277.5,46.5 290.8,46.5 304.1,46.5C304.2,39.6 294.9,30.1 286.7,27.5C286.6,28.5 286.5,29.3 286.5,30.3C278.3,30.5 270.3,30.3 263,26.4C261.1,25.4 259.8,23.1 258.3,21.5C256.3,19.3 254.7,16.6 252.3,15.2C247.4,12.3 242,10.3 236.9,7.8C224.3,1.7 211.3,-0.1 197.7,0zM249.6,29.4C250.3,29.4 250.9,29.6 251.6,30.2C252.6,31 253.6,31.9 254.8,33C253.3,33.8 252,34.5 250.8,35.1C249,36 247.6,35.4 246.5,34C245.6,32.8 245.5,31.7 246.8,30.7C247.7,30 248.6,29.4 249.6,29.4zM231.8,213C220.7,221.9 207.6,227.2 193.6,230.1C152.7,238.5 118.7,282.2 128.3,330.3C139.5,386.5 201.5,416.9 252.2,390.2C285,372.9 302.3,339.2 297.7,302.5C295.9,288.1 291,275.4 283.1,264.3L265.1,246.3C264.2,246.2 263.2,246.6 261.9,247.5C253.1,253.1 244.2,258.4 235.1,263.4C229.9,266.2 224.2,268.3 217.7,271.2C219.9,271.8 221,272.1 222.1,272.4C246.5,278.9 259.6,300.3 253.8,324.4C248.7,345.7 227,359.4 205.9,355.8C188.4,352.8 173.1,338.2 170.5,320.8C167.8,301.8 177.2,283.5 194.1,275.9C203.4,271.6 213,267.9 222.3,263.6C232.9,258.7 244.4,254.9 253.6,248.1C256.2,246.2 258.7,244.2 261,242.2L231.8,213z" - android:strokeColor="#00000000" - android:strokeWidth="1.3750788" /> - <path - android:fillAlpha="1" - android:fillColor="#ffffff" - android:pathData="m97.9,307.6c-7.8,2 -15.4,4.9 -23.4,7.5 3.9,-26.3 34.7,-50.6 60.8,-47.8 -8.1,10.9 -11.8,23.3 -12.7,35.6 -8.7,1.6 -16.8,2.7 -24.8,4.7" - android:strokeColor="#00000000" - android:strokeWidth="1.3750788" /> - <path - android:fillAlpha="1" - android:fillColor="#ffffff" - android:pathData="M177,111.6C175.7,111.6 174.4,111.7 173.1,111.7L247,185.6C248.1,183.2 249,180.6 249.9,177.9C252.2,170.4 251.8,159.3 248,152.1C234.7,126.6 206.6,111.3 177,111.6zM139.7,120.9C137.9,121.8 136.1,122.9 134.3,124C81.2,156.4 84.3,231 132.9,260.7C136.6,263 138.3,262.7 140.8,259.4C148.4,249.2 157.9,241.1 169,234.9C171.7,233.3 174.5,231.9 178.1,230C154,225.8 141.1,214.8 139.6,197.8C137.9,179.4 146.9,164.5 163.2,158.3C167,156.9 171,156.2 174.9,156.1L139.7,120.9zM212.5,193.7C212.5,195.8 212.3,197.9 211.8,200.1C209.9,210.5 203.6,218.1 195.8,224.8C208.5,221.9 219.6,216.9 228.6,209.7L212.5,193.7z" - android:strokeColor="#00000000" - android:strokeWidth="1.3750788" /> - <path - android:fillAlpha="1" - android:fillColor="#ffffff" - android:pathData="M78.4,59.6l262,262l-21.3,21.3l-262,-262z" - android:strokeWidth="0.82064575" /> -</vector> |