Misc clean up. move widgets from graph to widget package.

Bug: n/a
Test: robotests, rebuild
Change-Id: I910f355312d52e81a0bf57b46a3f267e1eb9882a
This commit is contained in:
Fan Zhang
2018-06-11 16:00:09 -07:00
parent ddd9283ee0
commit 670ce333ae
16 changed files with 65 additions and 54 deletions

View File

@@ -0,0 +1,96 @@
/*
* 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.settings.widget;
import android.annotation.Nullable;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import com.android.settingslib.R;
/**
* An extension of LinearLayout that automatically switches to vertical
* orientation when it can't fit its child views horizontally.
*
* Main logic in this class comes from {@link androidx.appcompat.widget.ButtonBarLayout}.
* Compared with {@link androidx.appcompat.widget.ButtonBarLayout}, this layout won't reverse
* children's order and won't update the minimum height
*/
public class BottomLabelLayout extends LinearLayout {
private static final String TAG = "BottomLabelLayout";
public BottomLabelLayout(Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final boolean isStacked = isStacked();
boolean needsRemeasure = false;
// If we're not stacked, make sure the measure spec is AT_MOST rather
// than EXACTLY. This ensures that we'll still get TOO_SMALL so that we
// know to stack the buttons.
final int initialWidthMeasureSpec;
if (!isStacked && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
// We'll need to remeasure again to fill excess space.
needsRemeasure = true;
} else {
initialWidthMeasureSpec = widthMeasureSpec;
}
super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
if (!isStacked) {
final int measuredWidth = getMeasuredWidthAndState();
final int measuredWidthState = measuredWidth & View.MEASURED_STATE_MASK;
if (measuredWidthState == View.MEASURED_STATE_TOO_SMALL) {
setStacked(true);
// Measure again in the new orientation.
needsRemeasure = true;
}
}
if (needsRemeasure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@VisibleForTesting
void setStacked(boolean stacked) {
setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
setGravity(stacked ? Gravity.START : Gravity.BOTTOM);
final View spacer = findViewById(R.id.spacer);
if (spacer != null) {
spacer.setVisibility(stacked ? View.GONE : View.VISIBLE);
}
}
private boolean isStacked() {
return getOrientation() == LinearLayout.VERTICAL;
}
}

View File

@@ -0,0 +1,315 @@
/*
* Copyright (C) 2016 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.settings.widget;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.Drawable;
import androidx.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.view.View;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settingslib.R;
public class UsageGraph extends View {
private static final int PATH_DELIM = -1;
public static final String LOG_TAG = "UsageGraph";
private final Paint mLinePaint;
private final Paint mFillPaint;
private final Paint mDottedPaint;
private final Drawable mDivider;
private final Drawable mTintedDivider;
private final int mDividerSize;
private final Path mPath = new Path();
// Paths in coordinates they are passed in.
private final SparseIntArray mPaths = new SparseIntArray();
// Paths in local coordinates for drawing.
private final SparseIntArray mLocalPaths = new SparseIntArray();
// Paths for projection in coordinates they are passed in.
private final SparseIntArray mProjectedPaths = new SparseIntArray();
// Paths for projection in local coordinates for drawing.
private final SparseIntArray mLocalProjectedPaths = new SparseIntArray();
private final int mCornerRadius;
private int mAccentColor;
private float mMaxX = 100;
private float mMaxY = 100;
private float mMiddleDividerLoc = .5f;
private int mMiddleDividerTint = -1;
private int mTopDividerTint = -1;
public UsageGraph(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final Resources resources = context.getResources();
mLinePaint = new Paint();
mLinePaint.setStyle(Style.STROKE);
mLinePaint.setStrokeCap(Cap.ROUND);
mLinePaint.setStrokeJoin(Join.ROUND);
mLinePaint.setAntiAlias(true);
mCornerRadius = resources.getDimensionPixelSize(R.dimen.usage_graph_line_corner_radius);
mLinePaint.setPathEffect(new CornerPathEffect(mCornerRadius));
mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.usage_graph_line_width));
mFillPaint = new Paint(mLinePaint);
mFillPaint.setStyle(Style.FILL);
mDottedPaint = new Paint(mLinePaint);
mDottedPaint.setStyle(Style.STROKE);
float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size);
float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval);
mDottedPaint.setStrokeWidth(dots * 3);
mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
TypedValue v = new TypedValue();
context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
mDivider = context.getDrawable(v.resourceId);
mTintedDivider = context.getDrawable(v.resourceId);
mDividerSize = resources.getDimensionPixelSize(R.dimen.usage_graph_divider_size);
}
void clearPaths() {
mPaths.clear();
mLocalPaths.clear();
mProjectedPaths.clear();
mLocalProjectedPaths.clear();
}
void setMax(int maxX, int maxY) {
final long startTime = System.currentTimeMillis();
mMaxX = maxX;
mMaxY = maxY;
calculateLocalPaths();
postInvalidate();
BatteryUtils.logRuntime(LOG_TAG, "setMax", startTime);
}
void setDividerLoc(int height) {
mMiddleDividerLoc = 1 - height / mMaxY;
}
void setDividerColors(int middleColor, int topColor) {
mMiddleDividerTint = middleColor;
mTopDividerTint = topColor;
}
public void addPath(SparseIntArray points) {
addPathAndUpdate(points, mPaths, mLocalPaths);
}
public void addProjectedPath(SparseIntArray points) {
addPathAndUpdate(points, mProjectedPaths, mLocalProjectedPaths);
}
private void addPathAndUpdate(
SparseIntArray points, SparseIntArray paths, SparseIntArray localPaths) {
final long startTime = System.currentTimeMillis();
for (int i = 0, size = points.size(); i < size; i++) {
paths.put(points.keyAt(i), points.valueAt(i));
}
// Add a delimiting value immediately after the last point.
paths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
calculateLocalPaths(paths, localPaths);
postInvalidate();
BatteryUtils.logRuntime(LOG_TAG, "addPathAndUpdate", startTime);
}
void setAccentColor(int color) {
mAccentColor = color;
mLinePaint.setColor(mAccentColor);
updateGradient();
postInvalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
final long startTime = System.currentTimeMillis();
super.onSizeChanged(w, h, oldw, oldh);
updateGradient();
calculateLocalPaths();
BatteryUtils.logRuntime(LOG_TAG, "onSizeChanged", startTime);
}
private void calculateLocalPaths() {
calculateLocalPaths(mPaths, mLocalPaths);
calculateLocalPaths(mProjectedPaths, mLocalProjectedPaths);
}
@VisibleForTesting
void calculateLocalPaths(SparseIntArray paths, SparseIntArray localPaths) {
final long startTime = System.currentTimeMillis();
if (getWidth() == 0) {
return;
}
localPaths.clear();
// Store the local coordinates of the most recent point.
int lx = 0;
int ly = PATH_DELIM;
boolean skippedLastPoint = false;
for (int i = 0; i < paths.size(); i++) {
int x = paths.keyAt(i);
int y = paths.valueAt(i);
if (y == PATH_DELIM) {
if (i == 1) {
localPaths.put(getX(x+1) - 1, getY(0));
continue;
}
if (i == paths.size() - 1 && skippedLastPoint) {
// Add back skipped point to complete the path.
localPaths.put(lx, ly);
}
skippedLastPoint = false;
localPaths.put(lx + 1, PATH_DELIM);
} else {
lx = getX(x);
ly = getY(y);
// Skip this point if it is not far enough from the last one added.
if (localPaths.size() > 0) {
int lastX = localPaths.keyAt(localPaths.size() - 1);
int lastY = localPaths.valueAt(localPaths.size() - 1);
if (lastY != PATH_DELIM && !hasDiff(lastX, lx) && !hasDiff(lastY, ly)) {
skippedLastPoint = true;
continue;
}
}
skippedLastPoint = false;
localPaths.put(lx, ly);
}
}
BatteryUtils.logRuntime(LOG_TAG, "calculateLocalPaths", startTime);
}
private boolean hasDiff(int x1, int x2) {
return Math.abs(x2 - x1) >= mCornerRadius;
}
private int getX(float x) {
return (int) (x / mMaxX * getWidth());
}
private int getY(float y) {
return (int) (getHeight() * (1 - (y / mMaxY)));
}
private void updateGradient() {
mFillPaint.setShader(
new LinearGradient(
0, 0, 0, getHeight(), getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
}
private int getColor(int color, float alphaScale) {
return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
}
@Override
protected void onDraw(Canvas canvas) {
final long startTime = System.currentTimeMillis();
// Draw lines across the top, middle, and bottom.
if (mMiddleDividerLoc != 0) {
drawDivider(0, canvas, mTopDividerTint);
}
drawDivider(
(int) ((canvas.getHeight() - mDividerSize) * mMiddleDividerLoc),
canvas,
mMiddleDividerTint);
drawDivider(canvas.getHeight() - mDividerSize, canvas, -1);
if (mLocalPaths.size() == 0 && mLocalProjectedPaths.size() == 0) {
return;
}
drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint);
drawFilledPath(canvas, mLocalPaths, mFillPaint);
drawLinePath(canvas, mLocalPaths, mLinePaint);
BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime);
}
private void drawLinePath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
if (localPaths.size() == 0) {
return;
}
mPath.reset();
mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
for (int i = 1; i < localPaths.size(); i++) {
int x = localPaths.keyAt(i);
int y = localPaths.valueAt(i);
if (y == PATH_DELIM) {
if (++i < localPaths.size()) {
mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
}
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, paint);
}
private void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) {
mPath.reset();
float lastStartX = localPaths.keyAt(0);
mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0));
for (int i = 1; i < localPaths.size(); i++) {
int x = localPaths.keyAt(i);
int y = localPaths.valueAt(i);
if (y == PATH_DELIM) {
mPath.lineTo(localPaths.keyAt(i - 1), getHeight());
mPath.lineTo(lastStartX, getHeight());
mPath.close();
if (++i < localPaths.size()) {
lastStartX = localPaths.keyAt(i);
mPath.moveTo(localPaths.keyAt(i), localPaths.valueAt(i));
}
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, paint);
}
private void drawDivider(int y, Canvas canvas, int tintColor) {
Drawable d = mDivider;
if (tintColor != -1) {
mTintedDivider.setTint(tintColor);
d = mTintedDivider;
}
d.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
d.draw(canvas);
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright (C) 2016 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.settings.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.settingslib.R;
public class UsageView extends FrameLayout {
private final UsageGraph mUsageGraph;
private final TextView[] mLabels;
private final TextView[] mBottomLabels;
public UsageView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.usage_view, this);
mUsageGraph = findViewById(R.id.usage_graph);
mLabels = new TextView[] {
findViewById(R.id.label_bottom),
findViewById(R.id.label_middle),
findViewById(R.id.label_top),
};
mBottomLabels = new TextView[] {
findViewById(R.id.label_start),
findViewById(R.id.label_end),
};
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UsageView, 0, 0);
if (a.hasValue(R.styleable.UsageView_sideLabels)) {
setSideLabels(a.getTextArray(R.styleable.UsageView_sideLabels));
}
if (a.hasValue(R.styleable.UsageView_bottomLabels)) {
setBottomLabels(a.getTextArray(R.styleable.UsageView_bottomLabels));
}
if (a.hasValue(R.styleable.UsageView_textColor)) {
int color = a.getColor(R.styleable.UsageView_textColor, 0);
for (TextView v : mLabels) {
v.setTextColor(color);
}
for (TextView v : mBottomLabels) {
v.setTextColor(color);
}
}
if (a.hasValue(R.styleable.UsageView_android_gravity)) {
int gravity = a.getInt(R.styleable.UsageView_android_gravity, 0);
if (gravity == Gravity.END) {
LinearLayout layout = findViewById(R.id.graph_label_group);
LinearLayout labels = findViewById(R.id.label_group);
// Swap the children order.
layout.removeView(labels);
layout.addView(labels);
// Set gravity.
labels.setGravity(Gravity.END);
// Swap the bottom space order.
LinearLayout bottomLabels = findViewById(R.id.bottom_label_group);
View bottomSpace = bottomLabels.findViewById(R.id.bottom_label_space);
bottomLabels.removeView(bottomSpace);
bottomLabels.addView(bottomSpace);
} else if (gravity != Gravity.START) {
throw new IllegalArgumentException("Unsupported gravity " + gravity);
}
}
mUsageGraph.setAccentColor(a.getColor(R.styleable.UsageView_android_colorAccent, 0));
a.recycle();
}
public void clearPaths() {
mUsageGraph.clearPaths();
}
public void addPath(SparseIntArray points) {
mUsageGraph.addPath(points);
}
public void addProjectedPath(SparseIntArray points) {
mUsageGraph.addProjectedPath(points);
}
public void configureGraph(int maxX, int maxY) {
mUsageGraph.setMax(maxX, maxY);
}
public void setAccentColor(int color) {
mUsageGraph.setAccentColor(color);
}
public void setDividerLoc(int dividerLoc) {
mUsageGraph.setDividerLoc(dividerLoc);
}
public void setDividerColors(int middleColor, int topColor) {
mUsageGraph.setDividerColors(middleColor, topColor);
}
public void setSideLabelWeights(float before, float after) {
setWeight(R.id.space1, before);
setWeight(R.id.space2, after);
}
private void setWeight(int id, float weight) {
View v = findViewById(id);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) v.getLayoutParams();
params.weight = weight;
v.setLayoutParams(params);
}
public void setSideLabels(CharSequence[] labels) {
if (labels.length != mLabels.length) {
throw new IllegalArgumentException("Invalid number of labels");
}
for (int i = 0; i < mLabels.length; i++) {
mLabels[i].setText(labels[i]);
}
}
public void setBottomLabels(CharSequence[] labels) {
if (labels.length != mBottomLabels.length) {
throw new IllegalArgumentException("Invalid number of labels");
}
for (int i = 0; i < mBottomLabels.length; i++) {
mBottomLabels[i].setText(labels[i]);
}
}
}