Add battery indicator to bluetooth icon

This cl creates BluetoothDeviceLayerDrawable, which contains two
drwable:
1. previous bluetooth device drawable
2. battery drawable showing the battery level

If connected bt device has battery level, we will use this
LayerDrawable both in bluetooth main page and detail page.

Also, battery drawable is reused from BatteryMeterDrawableBase, we
rotate base drawable by 90 degree and update the spec a little bit.

Bug: 63393322
Test: RunSettingsLibRoboTests

Change-Id: I55313940576523dcbeb65a6e20e41289da8a3e0c
This commit is contained in:
jackqdyulei 2017-08-09 09:57:05 -07:00
parent a2a1c156f5
commit d94a9388e8
5 changed files with 324 additions and 3 deletions

View File

@ -55,9 +55,17 @@
<dimen name="battery_height">14.5dp</dimen>
<dimen name="battery_width">9.5dp</dimen>
<dimen name="bt_battery_padding">2dp</dimen>
<!-- Margin on the right side of the system icon group on Keyguard. -->
<fraction name="battery_button_height_fraction">10.5%</fraction>
<!-- Ratio between height of button part and height of total -->
<fraction name="bt_battery_button_height_fraction">7.5%</fraction>
<!-- Ratio between width and height -->
<fraction name="bt_battery_ratio_fraction">45%</fraction>
<!-- Fraction value to smooth the edges of the battery icon. The path will be inset by this
fraction of a pixel.-->
<fraction name="battery_subpixel_smoothing_left">0%</fraction>

View File

@ -50,6 +50,7 @@ public class BatteryMeterDrawableBase extends Drawable {
protected final Paint mTextPaint;
protected final Paint mBoltPaint;
protected final Paint mPlusPaint;
protected float mButtonHeightFraction;
private int mLevel = -1;
private boolean mCharging;
@ -66,7 +67,6 @@ public class BatteryMeterDrawableBase extends Drawable {
private final int mIntrinsicWidth;
private final int mIntrinsicHeight;
private float mButtonHeightFraction;
private float mSubpixelSmoothingLeft;
private float mSubpixelSmoothingRight;
private float mTextHeight, mWarningTextHeight;
@ -298,7 +298,7 @@ public class BatteryMeterDrawableBase extends Drawable {
float drawFrac = (float) level / 100f;
final int height = mHeight;
final int width = (int) (ASPECT_RATIO * mHeight);
final int width = (int) (getAspectRatio() * mHeight);
final int px = (mWidth - width) / 2;
final int buttonHeight = Math.round(height * mButtonHeightFraction);
@ -329,7 +329,7 @@ public class BatteryMeterDrawableBase extends Drawable {
// define the battery shape
mShapePath.reset();
final float radius = RADIUS_RATIO * (mFrame.height() + buttonHeight);
final float radius = getRadiusRatio() * (mFrame.height() + buttonHeight);
mShapePath.setFillType(FillType.WINDING);
mShapePath.addRoundRect(mFrame, radius, radius, Direction.CW);
mShapePath.addRect(mButtonFrame, Direction.CW);
@ -469,4 +469,12 @@ public class BatteryMeterDrawableBase extends Drawable {
public int getCriticalLevel() {
return mCriticalLevel;
}
protected float getAspectRatio() {
return ASPECT_RATIO;
}
protected float getRadiusRatio() {
return RADIUS_RATIO;
}
}

View File

@ -0,0 +1,170 @@
/*
* 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.annotation.NonNull;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.support.annotation.VisibleForTesting;
import android.view.Gravity;
import android.view.View;
import com.android.settingslib.R;
import com.android.settingslib.Utils;
/**
* LayerDrawable contains the bluetooth device icon and battery gauge icon
*/
public class BluetoothDeviceLayerDrawable extends LayerDrawable {
private BluetoothDeviceLayerDrawableState mState;
private BluetoothDeviceLayerDrawable(@NonNull Drawable[] layers) {
super(layers);
}
/**
* Create the {@link LayerDrawable} that contains bluetooth device icon and battery icon.
* This is a vertical layout drawable while bluetooth icon at top and battery icon at bottom.
*
* @param context used to get the spec for icon
* @param resId represents the bluetooth device drawable
* @param batteryLevel the battery level for bluetooth device
*/
public static BluetoothDeviceLayerDrawable createLayerDrawable(Context context, int resId,
int batteryLevel) {
final Drawable deviceDrawable = context.getDrawable(resId);
final BatteryMeterDrawable batteryDrawable = new BatteryMeterDrawable(context,
R.color.meter_background_color, batteryLevel);
final int pad = context.getResources()
.getDimensionPixelSize(R.dimen.bt_battery_padding);
batteryDrawable.setPadding(0, pad, 0, pad);
final BluetoothDeviceLayerDrawable drawable = new BluetoothDeviceLayerDrawable(
new Drawable[]{deviceDrawable,
rotateDrawable(context.getResources(), batteryDrawable)});
// Set the bluetooth icon at the top
drawable.setLayerGravity(0 /* index of deviceDrawable */, Gravity.TOP);
// Set battery icon right below the bluetooth icon
drawable.setLayerInset(1 /* index of batteryDrawable */, 0,
deviceDrawable.getIntrinsicHeight(), 0, 0);
drawable.setConstantState(context, resId, batteryLevel);
return drawable;
}
/**
* Rotate the {@code drawable} by 90 degree clockwise and return rotated {@link Drawable}
*/
private static Drawable rotateDrawable(Resources res, Drawable drawable) {
// Get the bitmap from drawable
final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
// Create rotate matrix
final Matrix matrix = new Matrix();
matrix.postRotate(
res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR
? 90 : 270);
// Create new bitmap with rotate matrix
final Bitmap rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, true);
bitmap.recycle();
return new BitmapDrawable(res, rotateBitmap);
}
public void setConstantState(Context context, int resId, int batteryLevel) {
mState = new BluetoothDeviceLayerDrawableState(context, resId, batteryLevel);
}
@Override
public ConstantState getConstantState() {
return mState;
}
/**
* Battery gauge icon with new spec.
*/
@VisibleForTesting
static class BatteryMeterDrawable extends BatteryMeterDrawableBase {
private final float mAspectRatio;
public BatteryMeterDrawable(Context context, int frameColor, int batteryLevel) {
super(context, frameColor);
final Resources resources = context.getResources();
mButtonHeightFraction = resources.getFraction(
R.fraction.bt_battery_button_height_fraction, 1, 1);
mAspectRatio = resources.getFraction(R.fraction.bt_battery_ratio_fraction, 1, 1);
final int tintColor = Utils.getColorAttr(context, android.R.attr.colorControlNormal);
setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN));
setBatteryLevel(batteryLevel);
}
@Override
protected float getAspectRatio() {
return mAspectRatio;
}
@Override
protected float getRadiusRatio() {
// Remove the round edge
return 0;
}
}
/**
* {@link ConstantState} to restore the {@link BluetoothDeviceLayerDrawable}
*/
private static class BluetoothDeviceLayerDrawableState extends ConstantState {
Context context;
int resId;
int batteryLevel;
public BluetoothDeviceLayerDrawableState(Context context, int resId,
int batteryLevel) {
this.context = context;
this.resId = resId;
this.batteryLevel = batteryLevel;
}
@Override
public Drawable newDrawable() {
return createLayerDrawable(context, resId, batteryLevel);
}
@Override
public int getChangingConfigurations() {
return 0;
}
}
}

View File

@ -0,0 +1,86 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.VectorDrawable;
import com.android.settingslib.R;
import com.android.settingslib.SettingsLibRobolectricTestRunner;
import com.android.settingslib.TestConfig;
import com.android.settingslib.testutils.shadow.SettingsLibShadowResources;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@RunWith(SettingsLibRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
shadows = SettingsLibShadowResources.class)
public class BluetoothDeviceLayerDrawableTest {
private static final int RES_ID = R.drawable.ic_bt_cellphone;
private static final int BATTERY_LEVEL = 15;
private static final float TOLERANCE = 0.001f;
private Context mContext;
@Before
public void setUp() {
mContext = RuntimeEnvironment.application;
}
@Test
public void testCreateLayerDrawable_configCorrect() {
BluetoothDeviceLayerDrawable drawable = BluetoothDeviceLayerDrawable.createLayerDrawable(
mContext, RES_ID, BATTERY_LEVEL);
assertThat(drawable.getDrawable(0)).isInstanceOf(VectorDrawable.class);
assertThat(drawable.getDrawable(1)).isInstanceOf(BitmapDrawable.class);
assertThat(drawable.getLayerInsetTop(1)).isEqualTo(
drawable.getDrawable(0).getIntrinsicHeight());
}
@Test
public void testBatteryMeterDrawable_configCorrect() {
BluetoothDeviceLayerDrawable.BatteryMeterDrawable batteryDrawable =
new BluetoothDeviceLayerDrawable.BatteryMeterDrawable(mContext,
R.color.meter_background_color, BATTERY_LEVEL);
assertThat(batteryDrawable.getAspectRatio()).isWithin(TOLERANCE).of(0.45f);
assertThat(batteryDrawable.getRadiusRatio()).isWithin(TOLERANCE).of(0f);
assertThat(batteryDrawable.getBatteryLevel()).isEqualTo(BATTERY_LEVEL);
}
@Test
public void testConstantState_returnTwinBluetoothLayerDrawable() {
BluetoothDeviceLayerDrawable drawable = BluetoothDeviceLayerDrawable.createLayerDrawable(
mContext, RES_ID, BATTERY_LEVEL);
BluetoothDeviceLayerDrawable twinDrawable =
(BluetoothDeviceLayerDrawable) drawable.getConstantState().newDrawable();
assertThat(twinDrawable.getDrawable(0)).isEqualTo(drawable.getDrawable(0));
assertThat(twinDrawable.getDrawable(1)).isEqualTo(drawable.getDrawable(1));
assertThat(twinDrawable.getLayerInsetTop(1)).isEqualTo(
drawable.getLayerInsetTop(1));
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.testutils.shadow;
import static org.robolectric.internal.Shadow.directlyOn;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.support.annotation.ArrayRes;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadows.ShadowResources;
/**
* Shadow Resources to handle resource references that Robolectric shadows cannot
* handle because they are too new or private.
*/
@Implements(Resources.class)
public class SettingsLibShadowResources extends ShadowResources {
@RealObject
public Resources realResources;
@Implementation
public int[] getIntArray(@ArrayRes int id) throws NotFoundException {
// The Robolectric has resource mismatch for these values, so we need to stub it here
if (id == com.android.settingslib.R.array.batterymeter_bolt_points
|| id == com.android.settingslib.R.array.batterymeter_plus_points) {
return new int[2];
}
return directlyOn(realResources, Resources.class).getIntArray(id);
}
}