Checkpoint of data usage UI, graphs and lists.

Chart of network usage over time, with draggable "sweep" bars for
inspection region and warning/limits.  Talks with NetworkStatsService
for live data, and updates list of application usage as inspection
region changes.

Change-Id: I2a406e6776daf7d74143c07ec683c10fe711c277
This commit is contained in:
Jeff Sharkey
2011-05-30 16:19:56 -07:00
parent 32232fd9f2
commit ab2d8d3a38
13 changed files with 1052 additions and 0 deletions

View File

@@ -1,6 +1,8 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_STATIC_JAVA_LIBRARIES := guava
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)

View File

@@ -1128,6 +1128,19 @@
android:resource="@id/security_settings" />
</activity>
<activity android:name="Settings$DataUsageSummaryActivity"
android:label="@string/data_usage_summary_title">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.DATA_USAGE_SUMMARY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.DataUsageSummary" />
<meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
android:resource="@id/wireless_settings" />
</activity>
<receiver android:name=".widget.SettingsAppWidgetProvider"
android:label="@string/gadget_title"
android:exported="false"
@@ -1142,5 +1155,6 @@
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_info" />
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,33 @@
<?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="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/chart_container"
android:layout_width="match_parent"
android:layout_height="200dip" />
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1" />
</LinearLayout>

View File

@@ -3358,4 +3358,8 @@ found in the list of installed applications.</string>
<string name="hdcp_checking_title">HDCP checking</string>
<!-- HDCP checking dialog title, used for debug purposes only. [CHAR LIMIT=25] -->
<string name="hdcp_checking_dialog_title">Set HDCP checking behavior</string>
<!-- Activity title for network data usage summary. [CHAR LIMIT=25] -->
<string name="data_usage_summary_title">Data usage</string>
</resources>

View File

@@ -89,4 +89,9 @@
android:summary="@string/proxy_settings_summary" >
</PreferenceScreen>
<PreferenceScreen
android:fragment="com.android.settings.DataUsageSummary"
android:key="data_usage_summary"
android:title="@string/data_usage_summary_title" />
</PreferenceScreen>

View File

@@ -0,0 +1,353 @@
/*
* 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.
*/
package com.android.settings;
import static com.android.settings.widget.ChartView.buildChartParams;
import static com.android.settings.widget.ChartView.buildSweepParams;
import android.app.Fragment;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.INetworkStatsService;
import android.net.NetworkStats;
import android.net.NetworkStatsHistory;
import android.net.TrafficStats;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.text.format.DateUtils;
import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.android.settings.widget.ChartAxis;
import com.android.settings.widget.ChartGridView;
import com.android.settings.widget.ChartNetworkSeriesView;
import com.android.settings.widget.ChartSweepView;
import com.android.settings.widget.ChartSweepView.OnSweepListener;
import com.android.settings.widget.ChartView;
import com.android.settings.widget.InvertedChartAxis;
import com.google.android.collect.Lists;
import java.util.ArrayList;
import java.util.Collections;
public class DataUsageSummary extends Fragment {
private static final String TAG = "DataUsage";
// TODO: teach about wifi-vs-mobile data with tabs
private static final long KB_IN_BYTES = 1024;
private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
private static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
private INetworkStatsService mStatsService;
private ViewGroup mChartContainer;
private ListView mList;
private ChartAxis mAxisTime;
private ChartAxis mAxisData;
private ChartView mChart;
private ChartNetworkSeriesView mSeries;
private ChartSweepView mSweepTime1;
private ChartSweepView mSweepTime2;
private ChartSweepView mSweepDataWarn;
private ChartSweepView mSweepDataLimit;
private DataUsageAdapter mAdapter;
// TODO: persist warning/limit into policy service
private static final long DATA_WARN = (long) 3.2 * GB_IN_BYTES;
private static final long DATA_LIMIT = (long) 4.8 * GB_IN_BYTES;
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final Context context = inflater.getContext();
final long now = System.currentTimeMillis();
mStatsService = INetworkStatsService.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
mAxisTime = new TimeAxis();
mAxisData = new InvertedChartAxis(new DataAxis());
mChart = new ChartView(context, mAxisTime, mAxisData);
mChart.setPadding(20, 20, 20, 20);
mChart.addView(new ChartGridView(context, mAxisTime, mAxisData), buildChartParams());
mSeries = new ChartNetworkSeriesView(context, mAxisTime, mAxisData);
mChart.addView(mSeries, buildChartParams());
mSweepTime1 = new ChartSweepView(context, mAxisTime, now - DateUtils.DAY_IN_MILLIS * 14,
Color.parseColor("#ffffff"));
mSweepTime2 = new ChartSweepView(context, mAxisTime, now - DateUtils.DAY_IN_MILLIS * 7,
Color.parseColor("#ffffff"));
mSweepDataWarn = new ChartSweepView(
context, mAxisData, DATA_WARN, Color.parseColor("#f7931d"));
mSweepDataLimit = new ChartSweepView(
context, mAxisData, DATA_LIMIT, Color.parseColor("#be1d2c"));
mChart.addView(mSweepTime1, buildSweepParams());
mChart.addView(mSweepTime2, buildSweepParams());
mChart.addView(mSweepDataWarn, buildSweepParams());
mChart.addView(mSweepDataLimit, buildSweepParams());
mSeries.bindSweepRange(mSweepTime1, mSweepTime2);
mSweepTime1.addOnSweepListener(mSweepListener);
mSweepTime2.addOnSweepListener(mSweepListener);
mAdapter = new DataUsageAdapter();
final View view = inflater.inflate(R.layout.data_usage_summary, container, false);
mChartContainer = (ViewGroup) view.findViewById(R.id.chart_container);
mChartContainer.addView(mChart);
mList = (ListView) view.findViewById(R.id.list);
mList.setAdapter(mAdapter);
return view;
}
@Override
public void onResume() {
super.onResume();
updateSummaryData();
updateDetailData();
}
private void updateSummaryData() {
try {
final NetworkStatsHistory history = mStatsService.getHistoryForNetwork(
TrafficStats.TEMPLATE_MOBILE_ALL);
mSeries.bindNetworkStats(history);
} catch (RemoteException e) {
Log.w(TAG, "problem reading stats");
}
}
private void updateDetailData() {
final long sweep1 = mSweepTime1.getValue();
final long sweep2 = mSweepTime2.getValue();
final long start = Math.min(sweep1, sweep2);
final long end = Math.max(sweep1, sweep2);
try {
final NetworkStats stats = mStatsService.getSummaryForAllUid(
start, end, TrafficStats.TEMPLATE_MOBILE_ALL);
mAdapter.bindStats(stats);
} catch (RemoteException e) {
Log.w(TAG, "problem reading stats");
}
}
private OnSweepListener mSweepListener = new OnSweepListener() {
public void onSweep(ChartSweepView sweep, boolean sweepDone) {
// always update graph clip region
mSeries.invalidate();
// update detail list only when done sweeping
if (sweepDone) {
updateDetailData();
}
}
};
/**
* Adapter of applications, sorted by total usage descending.
*/
public static class DataUsageAdapter extends BaseAdapter {
private ArrayList<UsageRecord> mData = Lists.newArrayList();
private static class UsageRecord implements Comparable<UsageRecord> {
public int uid;
public long total;
/** {@inheritDoc} */
public int compareTo(UsageRecord another) {
return Long.compare(another.total, total);
}
}
public void bindStats(NetworkStats stats) {
mData.clear();
for (int i = 0; i < stats.length(); i++) {
final UsageRecord record = new UsageRecord();
record.uid = stats.uid[i];
record.total = stats.rx[i] + stats.tx[i];
mData.add(record);
}
Collections.sort(mData);
notifyDataSetChanged();
}
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
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);
}
final Context context = parent.getContext();
final PackageManager pm = context.getPackageManager();
final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
final TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
final UsageRecord record = mData.get(position);
text1.setText(pm.getNameForUid(record.uid));
text2.setText(Formatter.formatFileSize(context, record.total));
return convertView;
}
}
public static class TimeAxis implements ChartAxis {
private static final long TICK_INTERVAL = DateUtils.DAY_IN_MILLIS * 7;
private long mMin;
private long mMax;
private float mSize;
public TimeAxis() {
// TODO: hook up these ranges to policy service
mMax = System.currentTimeMillis();
mMin = mMax - DateUtils.DAY_IN_MILLIS * 30;
}
/** {@inheritDoc} */
public void setSize(float size) {
this.mSize = size;
}
/** {@inheritDoc} */
public float convertToPoint(long value) {
return (mSize * (value - mMin)) / (mMax - mMin);
}
/** {@inheritDoc} */
public long convertToValue(float point) {
return (long) (mMin + ((point * (mMax - mMin)) / mSize));
}
/** {@inheritDoc} */
public CharSequence getLabel(long value) {
// TODO: convert to string
return Long.toString(value);
}
/** {@inheritDoc} */
public float[] getTickPoints() {
// tick mark for every week
final int tickCount = (int) ((mMax - mMin) / TICK_INTERVAL);
final float[] tickPoints = new float[tickCount];
for (int i = 0; i < tickCount; i++) {
tickPoints[i] = convertToPoint(mMax - (TICK_INTERVAL * i));
}
return tickPoints;
}
}
// TODO: make data axis log-scale
public static class DataAxis implements ChartAxis {
private long mMin;
private long mMax;
private float mSize;
public DataAxis() {
// TODO: adapt ranges to show when history >5GB, and handle 4G
// interfaces with higher limits.
mMin = 0;
mMax = 5 * GB_IN_BYTES;
}
/** {@inheritDoc} */
public void setSize(float size) {
this.mSize = size;
}
/** {@inheritDoc} */
public float convertToPoint(long value) {
return (mSize * (value - mMin)) / (mMax - mMin);
}
/** {@inheritDoc} */
public long convertToValue(float point) {
return (long) (mMin + ((point * (mMax - mMin)) / mSize));
}
/** {@inheritDoc} */
public CharSequence getLabel(long value) {
// TODO: convert to string
return Long.toString(value);
}
/** {@inheritDoc} */
public float[] getTickPoints() {
final float[] tickPoints = new float[16];
long value = mMax;
float mult = 0.8f;
for (int i = 0; i < tickPoints.length; i++) {
tickPoints[i] = convertToPoint(value);
value = (long) (value * mult);
mult *= 0.9;
}
return tickPoints;
}
}
}

View File

@@ -343,4 +343,5 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler {
public static class AccountSyncSettingsInAddAccountActivity extends Settings { }
public static class CryptKeeperSettingsActivity extends Settings { }
public static class DeviceAdminSettingsActivity extends Settings { }
public static class DataUsageSummaryActivity extends Settings { }
}

View File

@@ -0,0 +1,34 @@
/*
* 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.
*/
package com.android.settings.widget;
/**
* Axis along a {@link ChartView} that knows how to convert between raw point
* and screen coordinate systems.
*/
public interface ChartAxis {
public void setSize(float size);
public float convertToPoint(long value);
public long convertToValue(float point);
public CharSequence getLabel(long value);
public float[] getTickPoints();
}

View File

@@ -0,0 +1,79 @@
/*
* 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.
*/
package com.android.settings.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.view.View;
import com.google.common.base.Preconditions;
/**
* Background of {@link ChartView} that renders grid lines as requested by
* {@link ChartAxis#getTickPoints()}.
*/
public class ChartGridView extends View {
private final ChartAxis mHoriz;
private final ChartAxis mVert;
private final Paint mPaintHoriz;
private final Paint mPaintVert;
public ChartGridView(Context context, ChartAxis horiz, ChartAxis vert) {
super(context);
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert");
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);
mPaintVert = new Paint();
mPaintVert.setColor(Color.parseColor("#28262c"));
mPaintVert.setStrokeWidth(1.0f);
mPaintVert.setStyle(Style.STROKE);
mPaintVert.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
final int width = getWidth();
final int height = getHeight();
final float[] vertTicks = mVert.getTickPoints();
for (float y : vertTicks) {
canvas.drawLine(0, y, width, y, mPaintVert);
}
final float[] horizTicks = mHoriz.getTickPoints();
for (float x : horizTicks) {
canvas.drawLine(x, 0, x, height, mPaintHoriz);
}
canvas.drawRect(0, 0, width, height, mPaintHoriz);
}
}

View File

@@ -0,0 +1,183 @@
/*
* 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.
*/
package com.android.settings.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.RectF;
import android.net.NetworkStatsHistory;
import android.util.Log;
import android.view.View;
import com.google.common.base.Preconditions;
/**
* {@link NetworkStatsHistory} series to render inside a {@link ChartView},
* using {@link ChartAxis} to map into screen coordinates.
*/
public class ChartNetworkSeriesView extends View {
private static final String TAG = "ChartNetworkSeriesView";
private static final boolean LOGD = false;
private final ChartAxis mHoriz;
private final ChartAxis mVert;
private final Paint mPaintStroke;
private final Paint mPaintFill;
private final Paint mPaintFillDisabled;
private NetworkStatsHistory mStats;
private Path mPathStroke;
private Path mPathFill;
private ChartSweepView mSweep1;
private ChartSweepView mSweep2;
public ChartNetworkSeriesView(Context context, ChartAxis horiz, ChartAxis vert) {
super(context);
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert");
mPaintStroke = new Paint();
mPaintStroke.setStrokeWidth(6.0f);
mPaintStroke.setColor(Color.parseColor("#24aae1"));
mPaintStroke.setStyle(Style.STROKE);
mPaintStroke.setAntiAlias(true);
mPaintFill = new Paint();
mPaintFill.setColor(Color.parseColor("#c050ade5"));
mPaintFill.setStyle(Style.FILL);
mPaintFill.setAntiAlias(true);
mPaintFillDisabled = new Paint();
mPaintFillDisabled.setColor(Color.parseColor("#88566abc"));
mPaintFillDisabled.setStyle(Style.FILL);
mPaintFillDisabled.setAntiAlias(true);
mPathStroke = new Path();
mPathFill = new Path();
}
public void bindNetworkStats(NetworkStatsHistory stats) {
mStats = stats;
}
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");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
generatePath();
}
/**
* Erase any existing {@link Path} and generate series outline based on
* currently bound {@link NetworkStatsHistory} data.
*/
private void generatePath() {
mPathStroke.reset();
mPathFill.reset();
// bail when not enough stats to render
if (mStats == null || mStats.bucketCount < 2) return;
final int width = getWidth();
final int height = getHeight();
boolean started = false;
float firstX = 0;
float lastX = 0;
float lastY = 0;
long totalData = 0;
for (int i = 0; i < mStats.bucketCount; i++) {
final float x = mHoriz.convertToPoint(mStats.bucketStart[i]);
final float y = mVert.convertToPoint(totalData);
// skip until we find first stats on screen
if (i > 0 && !started && x > 0) {
mPathStroke.moveTo(lastX, lastY);
mPathFill.moveTo(lastX, lastY);
started = true;
firstX = x;
}
if (started) {
mPathStroke.lineTo(x, y);
mPathFill.lineTo(x, y);
totalData += mStats.rx[i] + mStats.tx[i];
}
// skip if beyond view
if (x > width) break;
lastX = x;
lastY = y;
}
if (LOGD) {
final RectF bounds = new RectF();
mPathFill.computeBounds(bounds, true);
Log.d(TAG, "onLayout() rendered with bounds=" + bounds.toString());
}
// drop to bottom of graph from current location
mPathFill.lineTo(lastX, height);
mPathFill.lineTo(firstX, height);
}
@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.restoreToCount(save);
save = canvas.save();
canvas.clipRect(sweepRight, 0, getWidth(), getHeight());
canvas.drawPath(mPathFill, mPaintFillDisabled);
canvas.restoreToCount(save);
save = canvas.save();
canvas.clipRect(sweepLeft, 0, sweepRight, getHeight());
canvas.drawPath(mPathFill, mPaintFill);
canvas.drawPath(mPathStroke, mPaintStroke);
canvas.restoreToCount(save);
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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.
*/
package com.android.settings.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.view.MotionEvent;
import android.view.View;
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 {
private final Paint mPaintSweep;
private final Paint mPaintShadow;
private final ChartAxis mAxis;
private long mValue;
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);
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);
mPaintShadow = new Paint();
mPaintShadow.setColor(Color.BLACK);
mPaintShadow.setStrokeWidth(6.0f);
mPaintShadow.setStyle(Style.FILL_AND_STROKE);
mPaintShadow.setAntiAlias(true);
}
public void addOnSweepListener(OnSweepListener listener) {
mListener = listener;
}
private void dispatchOnSweep(boolean sweepDone) {
if (mListener != null) {
mListener.onSweep(this, sweepDone);
}
}
public ChartAxis getAxis() {
return mAxis;
}
public long getValue() {
return mValue;
}
public float getPoint() {
return mAxis.convertToPoint(mValue);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final View parent = (View) getParent();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mTracking = event.copy();
return true;
}
case MotionEvent.ACTION_MOVE: {
if (mHorizontal) {
setTranslationY(event.getRawY() - mTracking.getRawY());
final float point = (getTop() + getTranslationY() + (getHeight() / 2))
- parent.getPaddingTop();
mValue = mAxis.convertToValue(point);
dispatchOnSweep(false);
} else {
setTranslationX(event.getRawX() - mTracking.getRawX());
final float point = (getLeft() + getTranslationX() + (getWidth() / 2))
- parent.getPaddingLeft();
mValue = mAxis.convertToValue(point);
dispatchOnSweep(false);
}
return true;
}
case MotionEvent.ACTION_UP: {
mTracking = null;
setTranslationX(0);
setTranslationY(0);
requestLayout();
dispatchOnSweep(true);
return true;
}
default: {
return false;
}
}
}
@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);
}
@Override
protected void onDraw(Canvas canvas) {
// draw line across larger dimension
final int width = getWidth();
final int height = getHeight();
mHorizontal = width > height;
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, mPaintSweep);
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, mPaintSweep);
canvas.drawCircle(centerX, endY, 4.0f, mPaintShadow);
canvas.drawCircle(centerX, endY, 4.0f, mPaintSweep);
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.
*/
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.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
/**
* Container for two-dimensional chart, drawn with a combination of
* {@link ChartGridView}, {@link ChartNetworkSeriesView} and {@link ChartSweepView}
* children. The entire chart uses {@link ChartAxis} to map between raw values
* and screen coordinates.
*/
public class ChartView extends FrameLayout {
private static final String TAG = "ChartView";
// TODO: extend something that supports two-dimensional scrolling
private final ChartAxis mHoriz;
private final ChartAxis mVert;
private Rect mContent = new Rect();
public ChartView(Context context, ChartAxis horiz, ChartAxis vert) {
super(context);
mHoriz = checkNotNull(horiz, "missing horiz");
mVert = checkNotNull(vert, "missing vert");
setClipToPadding(false);
setClipChildren(false);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContent.set(l + getPaddingLeft(), t + getPaddingTop(), r - getPaddingRight(),
b - getPaddingBottom());
final int width = mContent.width();
final int height = mContent.height();
// no scrolling yet, so tell dimensions to fill exactly
mHoriz.setSize(width);
mVert.setSize(height);
final Rect parentRect = new Rect();
final Rect childRect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final LayoutParams params = (LayoutParams) child.getLayoutParams();
parentRect.set(mContent);
if (child instanceof ChartNetworkSeriesView || child instanceof ChartGridView) {
// series are always laid out to fill entire graph area
// TODO: handle scrolling for series larger than content area
Gravity.apply(params.gravity, width, height, parentRect, childRect);
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
} 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();
if (axis == mHoriz) {
parentRect.left = parentRect.right = (int) point + getPaddingLeft();
parentRect.bottom += child.getMeasuredWidth();
Gravity.apply(params.gravity, child.getMeasuredWidth(), parentRect.height(),
parentRect, childRect);
} else if (axis == mVert) {
parentRect.top = parentRect.bottom = (int) point + getPaddingTop();
parentRect.right += child.getMeasuredHeight();
Gravity.apply(params.gravity, parentRect.width(), child.getMeasuredHeight(),
parentRect, childRect);
} else {
throw new IllegalStateException("unexpected axis");
}
}
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
}
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

@@ -0,0 +1,59 @@
/*
* 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.
*/
package com.android.settings.widget;
/**
* Utility to invert another {@link ChartAxis}.
*/
public class InvertedChartAxis implements ChartAxis {
private final ChartAxis mWrapped;
private float mSize;
public InvertedChartAxis(ChartAxis wrapped) {
mWrapped = wrapped;
}
/** {@inheritDoc} */
public void setSize(float size) {
mSize = size;
mWrapped.setSize(size);
}
/** {@inheritDoc} */
public float convertToPoint(long value) {
return mSize - mWrapped.convertToPoint(value);
}
/** {@inheritDoc} */
public long convertToValue(float point) {
return mWrapped.convertToValue(mSize - point);
}
/** {@inheritDoc} */
public CharSequence getLabel(long value) {
return mWrapped.getLabel(value);
}
/** {@inheritDoc} */
public float[] getTickPoints() {
final float[] points = mWrapped.getTickPoints();
for (int i = 0; i < points.length; i++) {
points[i] = mSize - points[i];
}
return points;
}
}