Iterate on data usage chart UI.

Switched to inflating chart views from XML, using attributes for
configuration.  Start using drawable assets for chart components
instead of manually painting.  Include hand-cut assets, and animate
between states when touched to invoke.

Clamp sweeps to valid chart ranges and prepare for sweep labels.

Bug: 4768483, 4598460
Change-Id: Ic660c35bec826eb5e3f6a1dde3cc04d8c437ef2b
This commit is contained in:
Jeff Sharkey
2011-06-23 00:39:38 -07:00
parent aef3981e86
commit 52c3f4461b
27 changed files with 608 additions and 223 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_activated="true" android:drawable="@drawable/data_sweep_left_activated" />
<item android:state_activated="false" android:drawable="@drawable/data_sweep_left_default" />
</selector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_activated="true" android:drawable="@drawable/data_sweep_limit_activated" />
<item android:state_activated="false" android:drawable="@drawable/data_sweep_limit_default" />
</selector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_activated="true" android:drawable="@drawable/data_sweep_right_activated" />
<item android:state_activated="false" android:drawable="@drawable/data_sweep_right_default" />
</selector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_activated="true" android:drawable="@drawable/data_sweep_warning_activated" />
<item android:state_activated="false" android:drawable="@drawable/data_sweep_warning_default" />
</selector>

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<com.android.settings.widget.DataUsageChartView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
android:layout_width="match_parent"
android:layout_height="220dip"
android:padding="16dip">
<com.android.settings.widget.ChartGridView
android:id="@+id/grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="left|bottom"
settings:primaryDrawable="@drawable/data_grid_primary"
settings:secondaryDrawable="@drawable/data_grid_secondary"
settings:borderDrawable="@drawable/data_grid_border"
settings:labelColor="#24aae1" />
<com.android.settings.widget.ChartNetworkSeriesView
android:id="@+id/series"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="left|bottom"
settings:strokeColor="#24aae1"
settings:fillColor="#c050ade5"
settings:fillColorSecondary="#88566abc" />
<com.android.settings.widget.ChartSweepView
android:id="@+id/sweep_left"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
settings:sweepDrawable="@drawable/data_sweep_left"
settings:followAxis="horizontal"
settings:showLabel="false" />
<com.android.settings.widget.ChartSweepView
android:id="@+id/sweep_right"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
settings:sweepDrawable="@drawable/data_sweep_right"
settings:followAxis="horizontal"
settings:showLabel="false" />
<com.android.settings.widget.ChartSweepView
android:id="@+id/sweep_limit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
settings:sweepDrawable="@drawable/data_sweep_limit"
settings:followAxis="vertical"
settings:showLabel="true" />
<com.android.settings.widget.ChartSweepView
android:id="@+id/sweep_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
settings:sweepDrawable="@drawable/data_sweep_warning"
settings:followAxis="vertical"
settings:showLabel="true" />
</com.android.settings.widget.DataUsageChartView>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dip"
android:orientation="vertical">
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dip"
android:layout_marginTop="6dip"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@android:id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dip"
android:layout_marginBottom="6dip"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>

View File

@@ -49,4 +49,26 @@
<!-- Minimum tick width for each slice in the bar chart. -->
<attr name="minTickWidth" format="dimension" />
</declare-styleable>
<declare-styleable name="ChartSweepView">
<attr name="sweepDrawable" format="reference" />
<attr name="followAxis">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<attr name="showLabel" format="boolean" />
</declare-styleable>
<declare-styleable name="ChartGridView">
<attr name="primaryDrawable" format="reference" />
<attr name="secondaryDrawable" format="reference" />
<attr name="borderDrawable" format="reference" />
<attr name="labelColor" format="color" />
</declare-styleable>
<declare-styleable name="ChartNetworkSeriesView">
<attr name="strokeColor" format="color" />
<attr name="fillColor" format="color" />
<attr name="fillColorSecondary" format="color" />
</declare-styleable>
</resources>

View File

@@ -3456,7 +3456,7 @@ found in the list of installed applications.</string>
<!-- Subtitle of dialog for editing data usage cycle reset date. [CHAR LIMIT=32] -->
<string name="data_usage_cycle_editor_subtitle">Date of each month:</string>
<!-- Positive button title for data usage cycle editor, confirming that changes should be saved. [CHAR LIMIT=32] -->
<string name="data_usage_cycle_editor_positive">set</string>
<string name="data_usage_cycle_editor_positive">Set</string>
<!-- Title of dialog shown before user limits data usage. [CHAR LIMIT=48] -->
<string name="data_usage_limit_dialog_title">Limiting data usage</string>
@@ -3476,6 +3476,9 @@ found in the list of installed applications.</string>
<!-- Body of dialog shown when data usage has exceeded limit and has been disabled. [CHAR LIMIT=NONE] -->
<string name="data_usage_disabled_dialog">The specified data usage limit has been reached.\n\nAdditional data use may incur carrier charges.</string>
<!-- Dialog button indicating that data connection should be re-enabled. [CHAR LIMIT=28] -->
<string name="data_usage_disabled_dialog_enable">re-enable data</string>
<string name="data_usage_disabled_dialog_enable">Re-enable data</string>
<!-- Label displaying current network data usage warning threshold. [CHAR LIMIT=18] -->
<string name="data_usage_sweep_warning"><font size="32"><xliff:g id="number" example="128">%1$s</xliff:g></font> <font size="12"><xliff:g id="unit" example="KB">%2$s</xliff:g></font>\n<font size="12">warning</font></string>
</resources>

View File

@@ -27,7 +27,6 @@ import static android.net.NetworkTemplate.MATCH_MOBILE_3G_LOWER;
import static android.net.NetworkTemplate.MATCH_MOBILE_4G;
import static android.net.NetworkTemplate.MATCH_MOBILE_ALL;
import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import android.app.AlertDialog;
@@ -68,7 +67,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
@@ -204,6 +202,7 @@ public class DataUsageSummary extends Fragment {
mDataEnabled.setOnCheckedChangeListener(mDataEnabledListener);
mDisableAtLimit = new CheckBox(inflater.getContext());
mDisableAtLimit.setClickable(false);
mDisableAtLimitView = inflatePreference(inflater, mSwitches, mDisableAtLimit);
mDisableAtLimitView.setOnClickListener(mDisableAtLimitListener);
@@ -216,11 +215,8 @@ public class DataUsageSummary extends Fragment {
mCycleSpinner.setAdapter(mCycleAdapter);
mCycleSpinner.setOnItemSelectedListener(mCycleListener);
final int chartHeight = getResources().getDimensionPixelSize(
R.dimen.data_usage_chart_height);
mChart = new DataUsageChartView(context);
mChart = (DataUsageChartView) inflater.inflate(R.layout.data_usage_chart, mListView, false);
mChart.setListener(mChartListener);
mChart.setLayoutParams(new AbsListView.LayoutParams(MATCH_PARENT, chartHeight));
mListView.addHeaderView(mChart, null, false);
mAdapter = new DataUsageAdapter();
@@ -791,7 +787,7 @@ public class DataUsageSummary extends Fragment {
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(
android.R.layout.simple_list_item_2, parent, false);
R.layout.data_usage_item, parent, false);
}
final Context context = parent.getContext();
@@ -1080,7 +1076,7 @@ public class DataUsageSummary extends Fragment {
/**
* Set {@link android.R.id#title} for a preference view inflated with
* {@link #inflatePreference(LayoutInflater, View, View)}.
* {@link #inflatePreference(LayoutInflater, ViewGroup, View)}.
*/
private static void setPreferenceTitle(View parent, int resId) {
final TextView title = (TextView) parent.findViewById(android.R.id.title);

View File

@@ -29,6 +29,7 @@ public interface ChartAxis {
public long convertToValue(float point);
public CharSequence getLabel(long value);
public CharSequence getShortLabel(long value);
public float[] getTickPoints();

View File

@@ -17,12 +17,13 @@
package com.android.settings.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import com.android.settings.R;
import com.google.common.base.Preconditions;
/**
@@ -31,32 +32,42 @@ import com.google.common.base.Preconditions;
*/
public class ChartGridView extends View {
private final ChartAxis mHoriz;
private final ChartAxis mVert;
// TODO: eventually teach about drawing chart labels
private final Paint mPaintHoriz;
private final Paint mPaintVert;
private ChartAxis mHoriz;
private ChartAxis mVert;
public ChartGridView(Context context, ChartAxis horiz, ChartAxis vert) {
super(context);
private Drawable mPrimary;
private Drawable mSecondary;
private Drawable mBorder;
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert");
public ChartGridView(Context context) {
this(context, null, 0);
}
public ChartGridView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ChartGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setWillNotDraw(false);
// TODO: convert these colors to resources
mPaintHoriz = new Paint();
mPaintHoriz.setColor(Color.parseColor("#667bb5"));
mPaintHoriz.setStrokeWidth(2.0f);
mPaintHoriz.setStyle(Style.STROKE);
mPaintHoriz.setAntiAlias(true);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ChartGridView, defStyle, 0);
mPaintVert = new Paint();
mPaintVert.setColor(Color.parseColor("#28262c"));
mPaintVert.setStrokeWidth(1.0f);
mPaintVert.setStyle(Style.STROKE);
mPaintVert.setAntiAlias(true);
mPrimary = a.getDrawable(R.styleable.ChartGridView_primaryDrawable);
mSecondary = a.getDrawable(R.styleable.ChartGridView_secondaryDrawable);
mBorder = a.getDrawable(R.styleable.ChartGridView_borderDrawable);
// TODO: eventually read labelColor
a.recycle();
}
void init(ChartAxis horiz, ChartAxis vert) {
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert");
}
@Override
@@ -64,16 +75,28 @@ public class ChartGridView extends View {
final int width = getWidth();
final int height = getHeight();
final Drawable secondary = mSecondary;
final int secondaryHeight = mSecondary.getIntrinsicHeight();
final float[] vertTicks = mVert.getTickPoints();
for (float y : vertTicks) {
canvas.drawLine(0, y, width, y, mPaintVert);
final int bottom = (int) Math.min(y + secondaryHeight, height);
secondary.setBounds(0, (int) y, width, bottom);
secondary.draw(canvas);
}
final Drawable primary = mPrimary;
final int primaryWidth = mPrimary.getIntrinsicWidth();
final int primaryHeight = mPrimary.getIntrinsicHeight();
final float[] horizTicks = mHoriz.getTickPoints();
for (float x : horizTicks) {
canvas.drawLine(x, 0, x, height, mPaintHoriz);
final int right = (int) Math.min(x + primaryWidth, width);
primary.setBounds((int) x, 0, right, height);
primary.draw(canvas);
}
canvas.drawRect(0, 0, width, height, mPaintHoriz);
mBorder.setBounds(0, 0, width, height);
mBorder.draw(canvas);
}
}

View File

@@ -17,6 +17,7 @@
package com.android.settings.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
@@ -24,9 +25,11 @@ import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.RectF;
import android.net.NetworkStatsHistory;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.android.settings.R;
import com.google.common.base.Preconditions;
/**
@@ -37,35 +40,54 @@ public class ChartNetworkSeriesView extends View {
private static final String TAG = "ChartNetworkSeriesView";
private static final boolean LOGD = true;
private final ChartAxis mHoriz;
private final ChartAxis mVert;
private ChartAxis mHoriz;
private ChartAxis mVert;
private Paint mPaintStroke;
private Paint mPaintFill;
private Paint mPaintFillDisabled;
private Paint mPaintFillSecondary;
private NetworkStatsHistory mStats;
private Path mPathStroke;
private Path mPathFill;
private ChartSweepView mSweep1;
private ChartSweepView mSweep2;
private long mPrimaryLeft;
private long mPrimaryRight;
public ChartNetworkSeriesView(Context context, ChartAxis horiz, ChartAxis vert) {
super(context);
public ChartNetworkSeriesView(Context context) {
this(context, null, 0);
}
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert");
public ChartNetworkSeriesView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
setChartColor(Color.parseColor("#24aae1"), Color.parseColor("#c050ade5"),
Color.parseColor("#88566abc"));
public ChartNetworkSeriesView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ChartNetworkSeriesView, defStyle, 0);
final int stroke = a.getColor(R.styleable.ChartNetworkSeriesView_strokeColor, Color.RED);
final int fill = a.getColor(R.styleable.ChartNetworkSeriesView_fillColor, Color.RED);
final int fillSecondary = a.getColor(
R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED);
setChartColor(stroke, fill, fillSecondary);
a.recycle();
mPathStroke = new Path();
mPathFill = new Path();
}
public void setChartColor(int stroke, int fill, int disabled) {
void init(ChartAxis horiz, ChartAxis vert) {
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert");
}
public void setChartColor(int stroke, int fill, int fillSecondary) {
mPaintStroke = new Paint();
mPaintStroke.setStrokeWidth(6.0f);
mPaintStroke.setColor(stroke);
@@ -77,10 +99,10 @@ public class ChartNetworkSeriesView extends View {
mPaintFill.setStyle(Style.FILL);
mPaintFill.setAntiAlias(true);
mPaintFillDisabled = new Paint();
mPaintFillDisabled.setColor(disabled);
mPaintFillDisabled.setStyle(Style.FILL);
mPaintFillDisabled.setAntiAlias(true);
mPaintFillSecondary = new Paint();
mPaintFillSecondary.setColor(fillSecondary);
mPaintFillSecondary.setStyle(Style.FILL);
mPaintFillSecondary.setAntiAlias(true);
}
public void bindNetworkStats(NetworkStatsHistory stats) {
@@ -90,12 +112,10 @@ public class ChartNetworkSeriesView extends View {
mPathFill.reset();
}
public void bindSweepRange(ChartSweepView sweep1, ChartSweepView sweep2) {
// TODO: generalize to support vertical sweeps
// TODO: enforce that both sweeps are along same dimension
mSweep1 = Preconditions.checkNotNull(sweep1, "missing sweep1");
mSweep2 = Preconditions.checkNotNull(sweep2, "missing sweep2");
public void setPrimaryRange(long left, long right) {
mPrimaryLeft = left;
mPrimaryRight = right;
invalidate();
}
@Override
@@ -168,27 +188,20 @@ public class ChartNetworkSeriesView extends View {
@Override
protected void onDraw(Canvas canvas) {
// clip to sweep area
final float sweep1 = mSweep1.getPoint();
final float sweep2 = mSweep2.getPoint();
final float sweepLeft = Math.min(sweep1, sweep2);
final float sweepRight = Math.max(sweep1, sweep2);
int save;
save = canvas.save();
canvas.clipRect(0, 0, sweepLeft, getHeight());
canvas.drawPath(mPathFill, mPaintFillDisabled);
canvas.clipRect(0, 0, mPrimaryLeft, getHeight());
canvas.drawPath(mPathFill, mPaintFillSecondary);
canvas.restoreToCount(save);
save = canvas.save();
canvas.clipRect(sweepRight, 0, getWidth(), getHeight());
canvas.drawPath(mPathFill, mPaintFillDisabled);
canvas.clipRect(mPrimaryRight, 0, getWidth(), getHeight());
canvas.drawPath(mPathFill, mPaintFillSecondary);
canvas.restoreToCount(save);
save = canvas.save();
canvas.clipRect(sweepLeft, 0, sweepRight, getHeight());
canvas.clipRect(mPrimaryLeft, 0, mPrimaryRight, getHeight());
canvas.drawPath(mPathFill, mPaintFill);
canvas.drawPath(mPathStroke, mPaintStroke);
canvas.restoreToCount(save);

View File

@@ -17,63 +17,79 @@
package com.android.settings.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.MathUtils;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import com.android.settings.R;
import com.google.common.base.Preconditions;
/**
* Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which
* a user can drag.
*/
public class ChartSweepView extends View {
public class ChartSweepView extends FrameLayout {
private final Paint mPaintSweep;
private final Paint mPaintSweepDisabled;
private final Paint mPaintShadow;
// TODO: paint label when requested
private final ChartAxis mAxis;
private Drawable mSweep;
private int mFollowAxis;
private boolean mShowLabel;
private ChartAxis mAxis;
private long mValue;
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
public interface OnSweepListener {
public void onSweep(ChartSweepView sweep, boolean sweepDone);
}
private OnSweepListener mListener;
private boolean mHorizontal;
private MotionEvent mTracking;
public ChartSweepView(Context context, ChartAxis axis, long value, int color) {
super(context);
public ChartSweepView(Context context) {
this(context, null, 0);
}
public ChartSweepView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ChartSweepView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ChartSweepView, defStyle, 0);
setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable));
setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1));
setShowLabel(a.getBoolean(R.styleable.ChartSweepView_showLabel, false));
a.recycle();
setClipToPadding(false);
setClipChildren(false);
setWillNotDraw(false);
}
void init(ChartAxis axis) {
mAxis = Preconditions.checkNotNull(axis, "missing axis");
mValue = value;
}
mPaintSweep = new Paint();
mPaintSweep.setColor(color);
mPaintSweep.setStrokeWidth(3.0f);
mPaintSweep.setStyle(Style.FILL_AND_STROKE);
mPaintSweep.setAntiAlias(true);
mPaintSweepDisabled = new Paint();
mPaintSweepDisabled.setColor(color);
mPaintSweepDisabled.setStrokeWidth(1.5f);
mPaintSweepDisabled.setStyle(Style.FILL_AND_STROKE);
mPaintSweepDisabled.setPathEffect(new DashPathEffect(new float[] { 5, 5 }, 0));
mPaintSweepDisabled.setAntiAlias(true);
mPaintShadow = new Paint();
mPaintShadow.setColor(Color.BLACK);
mPaintShadow.setStrokeWidth(6.0f);
mPaintShadow.setStyle(Style.FILL_AND_STROKE);
mPaintShadow.setAntiAlias(true);
public int getFollowAxis() {
return mFollowAxis;
}
public void getExtraMargins(Rect rect) {
mSweep.getPadding(rect);
}
public void addOnSweepListener(OnSweepListener listener) {
@@ -86,6 +102,56 @@ public class ChartSweepView extends View {
}
}
public void setSweepDrawable(Drawable sweep) {
if (mSweep != null) {
mSweep.setCallback(null);
unscheduleDrawable(mSweep);
}
if (sweep != null) {
sweep.setCallback(this);
if (sweep.isStateful()) {
sweep.setState(getDrawableState());
}
sweep.setVisible(getVisibility() == VISIBLE, false);
mSweep = sweep;
} else {
mSweep = null;
}
invalidate();
}
public void setFollowAxis(int followAxis) {
mFollowAxis = followAxis;
}
public void setShowLabel(boolean showLabel) {
mShowLabel = showLabel;
invalidate();
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (mSweep != null) {
mSweep.jumpToCurrentState();
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (mSweep != null) {
mSweep.setVisible(visibility == VISIBLE, false);
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return who == mSweep || super.verifyDrawable(who);
}
public ChartAxis getAxis() {
return mAxis;
}
@@ -115,14 +181,24 @@ public class ChartSweepView extends View {
case MotionEvent.ACTION_MOVE: {
getParent().requestDisallowInterceptTouchEvent(true);
if (mHorizontal) {
setTranslationY(event.getRawY() - mTracking.getRawY());
if (mFollowAxis == VERTICAL) {
final float chartHeight = parent.getHeight() - parent.getPaddingTop()
- parent.getPaddingBottom();
final float translationY = MathUtils.constrain(
event.getRawY() - mTracking.getRawY(), -getTop(),
chartHeight - getTop());
setTranslationY(translationY);
final float point = (getTop() + getTranslationY() + (getHeight() / 2))
- parent.getPaddingTop();
mValue = mAxis.convertToValue(point);
dispatchOnSweep(false);
} else {
setTranslationX(event.getRawX() - mTracking.getRawX());
final float chartWidth = parent.getWidth() - parent.getPaddingLeft()
- parent.getPaddingRight();
final float translationX = MathUtils.constrain(
event.getRawX() - mTracking.getRawX(), -getLeft(),
chartWidth - getLeft());
setTranslationX(translationX);
final float point = (getLeft() + getTranslationX() + (getWidth() / 2))
- parent.getPaddingLeft();
mValue = mAxis.convertToValue(point);
@@ -144,41 +220,26 @@ public class ChartSweepView extends View {
}
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (mSweep.isStateful()) {
mSweep.setState(getDrawableState());
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// need at least 50px in each direction for grippies
// TODO: provide this value through params
setMeasuredDimension(50, 50);
setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight());
}
@Override
protected void onDraw(Canvas canvas) {
// draw line across larger dimension
final int width = getWidth();
final int height = getHeight();
mHorizontal = width > height;
final Paint linePaint = isEnabled() ? mPaintSweep : mPaintSweepDisabled;
if (mHorizontal) {
final int centerY = height / 2;
final int endX = width - height;
canvas.drawLine(0, centerY, endX, centerY, mPaintShadow);
canvas.drawLine(0, centerY, endX, centerY, linePaint);
canvas.drawCircle(endX, centerY, 4.0f, mPaintShadow);
canvas.drawCircle(endX, centerY, 4.0f, mPaintSweep);
} else {
final int centerX = width / 2;
final int endY = height - width;
canvas.drawLine(centerX, 0, centerX, endY, mPaintShadow);
canvas.drawLine(centerX, 0, centerX, endY, linePaint);
canvas.drawCircle(centerX, endY, 4.0f, mPaintShadow);
canvas.drawCircle(centerX, endY, 4.0f, mPaintSweep);
}
mSweep.setBounds(0, 0, width, height);
mSweep.draw(canvas);
}
}

View File

@@ -16,13 +16,11 @@
package com.android.settings.widget;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context;
import android.graphics.Rect;
import android.util.Log;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
@@ -38,21 +36,31 @@ public class ChartView extends FrameLayout {
// TODO: extend something that supports two-dimensional scrolling
final ChartAxis mHoriz;
final ChartAxis mVert;
ChartAxis mHoriz;
ChartAxis mVert;
private Rect mContent = new Rect();
public ChartView(Context context, ChartAxis horiz, ChartAxis vert) {
super(context);
public ChartView(Context context) {
this(context, null, 0);
}
mHoriz = checkNotNull(horiz, "missing horiz");
mVert = checkNotNull(vert, "missing vert");
public ChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setClipToPadding(false);
setClipChildren(false);
}
void init(ChartAxis horiz, ChartAxis vert) {
mHoriz = checkNotNull(horiz, "missing horiz");
mVert = checkNotNull(vert, "missing vert");
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContent.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(),
@@ -66,6 +74,7 @@ public class ChartView extends FrameLayout {
final Rect parentRect = new Rect();
final Rect childRect = new Rect();
final Rect extraMargins = new Rect();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
@@ -82,23 +91,22 @@ public class ChartView extends FrameLayout {
} else if (child instanceof ChartSweepView) {
// sweep is always placed along specific dimension
final ChartSweepView sweep = (ChartSweepView) child;
final ChartAxis axis = sweep.getAxis();
final float point = sweep.getPoint();
sweep.getExtraMargins(extraMargins);
if (axis == mHoriz) {
if (sweep.getFollowAxis() == ChartSweepView.HORIZONTAL) {
parentRect.left = parentRect.right = (int) point + getPaddingLeft();
parentRect.bottom += child.getMeasuredWidth();
parentRect.top -= extraMargins.top;
parentRect.bottom += extraMargins.bottom;
Gravity.apply(params.gravity, child.getMeasuredWidth(), parentRect.height(),
parentRect, childRect);
} else if (axis == mVert) {
} else {
parentRect.top = parentRect.bottom = (int) point + getPaddingTop();
parentRect.right += child.getMeasuredHeight();
parentRect.left -= extraMargins.left;
parentRect.right += extraMargins.right;
Gravity.apply(params.gravity, parentRect.width(), child.getMeasuredHeight(),
parentRect, childRect);
} else {
throw new IllegalStateException("unexpected axis");
}
}
@@ -106,16 +114,4 @@ public class ChartView extends FrameLayout {
}
}
public static LayoutParams buildChartParams() {
final LayoutParams params = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
params.gravity = Gravity.LEFT | Gravity.BOTTOM;
return params;
}
public static LayoutParams buildSweepParams() {
final LayoutParams params = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
params.gravity = Gravity.CENTER;
return params;
}
}

View File

@@ -17,12 +17,14 @@
package com.android.settings.widget;
import android.content.Context;
import android.graphics.Color;
import android.net.NetworkPolicy;
import android.net.NetworkStatsHistory;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.android.settings.R;
import com.android.settings.widget.ChartSweepView.OnSweepListener;
/**
@@ -35,13 +37,16 @@ public class DataUsageChartView extends ChartView {
private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
private static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
// TODO: enforce that sweeps cant cross each other
// TODO: limit sweeps at graph boundaries
private ChartGridView mGrid;
private ChartNetworkSeriesView mSeries;
// TODO: limit sweeps at graph boundaries
private ChartSweepView mSweepTime1;
private ChartSweepView mSweepTime2;
private ChartSweepView mSweepDataWarn;
private ChartSweepView mSweepDataLimit;
private ChartSweepView mSweepLeft;
private ChartSweepView mSweepRight;
private ChartSweepView mSweepWarning;
private ChartSweepView mSweepLimit;
public interface DataUsageChartListener {
public void onInspectRangeChanged();
@@ -51,46 +56,58 @@ public class DataUsageChartView extends ChartView {
private DataUsageChartListener mListener;
private static ChartAxis buildTimeAxis() {
return new TimeAxis();
}
private static ChartAxis buildDataAxis() {
return new InvertedChartAxis(new DataAxis());
}
public DataUsageChartView(Context context) {
super(context, buildTimeAxis(), buildDataAxis());
setPadding(20, 20, 20, 20);
addView(new ChartGridView(context, mHoriz, mVert), buildChartParams());
mSeries = new ChartNetworkSeriesView(context, mHoriz, mVert);
addView(mSeries, buildChartParams());
mSweepTime1 = new ChartSweepView(context, mHoriz, 0L, Color.parseColor("#ffffff"));
mSweepTime2 = new ChartSweepView(context, mHoriz, 0L, Color.parseColor("#ffffff"));
mSweepDataWarn = new ChartSweepView(context, mVert, 0L, Color.parseColor("#f7931d"));
mSweepDataLimit = new ChartSweepView(context, mVert, 0L, Color.parseColor("#be1d2c"));
addView(mSweepTime1, buildSweepParams());
addView(mSweepTime2, buildSweepParams());
addView(mSweepDataWarn, buildSweepParams());
addView(mSweepDataLimit, buildSweepParams());
mSeries.bindSweepRange(mSweepTime1, mSweepTime2);
mSweepDataWarn.addOnSweepListener(mWarningListener);
mSweepDataLimit.addOnSweepListener(mLimitListener);
mSweepTime1.addOnSweepListener(mSweepListener);
mSweepTime2.addOnSweepListener(mSweepListener);
mSweepDataWarn.setVisibility(View.INVISIBLE);
mSweepDataLimit.setVisibility(View.INVISIBLE);
this(context, null, 0);
}
public DataUsageChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DataUsageChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(new TimeAxis(), new InvertedChartAxis(new DataAxis()));
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mGrid = (ChartGridView) findViewById(R.id.grid);
mSeries = (ChartNetworkSeriesView) findViewById(R.id.series);
mSweepLeft = (ChartSweepView) findViewById(R.id.sweep_left);
mSweepRight = (ChartSweepView) findViewById(R.id.sweep_right);
mSweepLimit = (ChartSweepView) findViewById(R.id.sweep_limit);
mSweepWarning = (ChartSweepView) findViewById(R.id.sweep_warning);
mSweepLeft.addOnSweepListener(mSweepListener);
mSweepRight.addOnSweepListener(mSweepListener);
mSweepWarning.addOnSweepListener(mWarningListener);
mSweepLimit.addOnSweepListener(mLimitListener);
// tell everyone about our axis
mGrid.init(mHoriz, mVert);
mSeries.init(mHoriz, mVert);
mSweepLeft.init(mHoriz);
mSweepRight.init(mHoriz);
mSweepWarning.init(mVert);
mSweepLimit.init(mVert);
setActivated(false);
}
@Override
public void setActivated(boolean activated) {
super.setActivated(activated);
mSweepLeft.setEnabled(activated);
mSweepRight.setEnabled(activated);
mSweepWarning.setEnabled(activated);
mSweepLimit.setEnabled(activated);
}
@Deprecated
public void setChartColor(int stroke, int fill, int disabled) {
mSeries.setChartColor(stroke, fill, disabled);
}
@@ -105,36 +122,46 @@ public class DataUsageChartView extends ChartView {
public void bindNetworkPolicy(NetworkPolicy policy) {
if (policy == null) {
mSweepDataLimit.setVisibility(View.INVISIBLE);
mSweepDataWarn.setVisibility(View.INVISIBLE);
mSweepLimit.setVisibility(View.INVISIBLE);
mSweepWarning.setVisibility(View.INVISIBLE);
return;
}
if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
mSweepDataLimit.setVisibility(View.VISIBLE);
mSweepDataLimit.setValue(policy.limitBytes);
mSweepDataLimit.setEnabled(true);
mSweepLimit.setVisibility(View.VISIBLE);
mSweepLimit.setValue(policy.limitBytes);
mSweepLimit.setEnabled(true);
} else {
// TODO: set limit default based on axis maximum
mSweepDataLimit.setVisibility(View.VISIBLE);
mSweepDataLimit.setValue(5 * GB_IN_BYTES);
mSweepDataLimit.setEnabled(false);
mSweepLimit.setVisibility(View.VISIBLE);
mSweepLimit.setValue(5 * GB_IN_BYTES);
mSweepLimit.setEnabled(false);
}
if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
mSweepDataWarn.setVisibility(View.VISIBLE);
mSweepDataWarn.setValue(policy.warningBytes);
mSweepWarning.setVisibility(View.VISIBLE);
mSweepWarning.setValue(policy.warningBytes);
} else {
mSweepDataWarn.setVisibility(View.INVISIBLE);
mSweepWarning.setVisibility(View.INVISIBLE);
}
requestLayout();
// TODO: eventually remove this; was to work around lack of sweep clamping
if (policy.limitBytes < -1 || policy.limitBytes > 5 * GB_IN_BYTES) {
policy.limitBytes = 5 * GB_IN_BYTES;
mLimitListener.onSweep(mSweepLimit, true);
}
if (policy.warningBytes < -1 || policy.warningBytes > 5 * GB_IN_BYTES) {
policy.warningBytes = 4 * GB_IN_BYTES;
mWarningListener.onSweep(mSweepWarning, true);
}
}
private OnSweepListener mSweepListener = new OnSweepListener() {
public void onSweep(ChartSweepView sweep, boolean sweepDone) {
// always update graph clip region
mSeries.invalidate();
mSeries.setPrimaryRange(mSweepLeft.getValue(), mSweepRight.getValue());
// update detail list only when done sweeping
if (sweepDone && mListener != null) {
@@ -159,24 +186,39 @@ public class DataUsageChartView extends ChartView {
}
};
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isActivated()) return false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
return true;
}
case MotionEvent.ACTION_UP: {
setActivated(true);
return true;
}
default: {
return false;
}
}
}
/**
* Return current inspection range (start and end time) based on internal
* {@link ChartSweepView} positions.
*/
public long[] getInspectRange() {
final long sweep1 = mSweepTime1.getValue();
final long sweep2 = mSweepTime2.getValue();
final long start = Math.min(sweep1, sweep2);
final long end = Math.max(sweep1, sweep2);
final long start = mSweepLeft.getValue();
final long end = mSweepRight.getValue();
return new long[] { start, end };
}
public long getWarningBytes() {
return mSweepDataWarn.getValue();
return mSweepWarning.getValue();
}
public long getLimitBytes() {
return mSweepDataLimit.getValue();
return mSweepLimit.getValue();
}
/**
@@ -192,8 +234,9 @@ public class DataUsageChartView extends ChartView {
final long sweepMax = Math.min(end, dataBoundary);
final long sweepMin = Math.max(start, (sweepMax - DateUtils.WEEK_IN_MILLIS));
mSweepTime1.setValue(sweepMin);
mSweepTime2.setValue(sweepMax);
mSweepLeft.setValue(sweepMin);
mSweepRight.setValue(sweepMax);
mSeries.setPrimaryRange(sweepMin, sweepMax);
requestLayout();
mSeries.generatePath();
@@ -239,6 +282,12 @@ public class DataUsageChartView extends ChartView {
return Long.toString(value);
}
/** {@inheritDoc} */
public CharSequence getShortLabel(long value) {
// TODO: convert to string
return Long.toString(value);
}
/** {@inheritDoc} */
public float[] getTickPoints() {
// tick mark for every week
@@ -299,6 +348,16 @@ public class DataUsageChartView extends ChartView {
/** {@inheritDoc} */
public CharSequence getLabel(long value) {
// TODO: use exploded string here
// TODO: convert to string
return Long.toString(value);
}
/** {@inheritDoc} */
public CharSequence getShortLabel(long value) {
// TODO: convert to string
return Long.toString(value);
}

View File

@@ -53,6 +53,11 @@ public class InvertedChartAxis implements ChartAxis {
return mWrapped.getLabel(value);
}
/** {@inheritDoc} */
public CharSequence getShortLabel(long value) {
return mWrapped.getShortLabel(value);
}
/** {@inheritDoc} */
public float[] getTickPoints() {
final float[] points = mWrapped.getTickPoints();