Logo Icon Logo
A Crowd-sourced Cookbook on Writing Great Android® Apps
GitHub logo Twitter logo OReilly Book Cover Art

Handling Orientation Changes: From ListView Data Values to Landscape Charting

Published? true
FormatLanguage: WikiFormat

Problem:

Accomplish view changes on device orientation changes. For example, data values to be plotted are contained in a Portrait ListView, and upon device orientation a graphical display of the data values in a chart/plot is displayed.

Solution:

Handle physical device orientation changes. Android emulator Control-F11 key combination will result in a Portrait to Landscape orientation change. A new View object is created on orientation changes. The Android method onConfigurationChanged(Configuration newConfig) can be overriden to accomodate for orientation changes.

Most important is to modify the AndroidManifest.xml to allow for the following: android:configChanges="orientation|keyboardHidden"

Discussion:

In this example, data values to be plotted are contained in a Portrait ListView. When the device/emualator is changed to counter-clockwise, a new Intent is launched to change to a plot/charting View to graphically display the data values. Charting is accomplished using the excellent DroidCharts package (http://code.google.com/p/droidcharts/).

The portrait version looks like this:

Rotate your device to landscape and you'll see this:

First is the AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest
	xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.examples"
	android:versionCode="1"
	android:versionName="1.0">

    <uses-sdk android:minSdkVersion="13" android:targetSdkVersion="17"/>

	<application
		android:icon="@drawable/icon"
		android:label="@string/app_name"
		android:allowBackup="false">
		
	    <!--  Android 13 or later, configChange must include screenSize -->
		<activity
			android:name=".DemoList"
			android:label="@string/app_name"
			android:configChanges="orientation|screenSize|keyboardHidden"
			>
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>
		<activity
			android:name=".DemoCharts"
			android:configChanges="orientation|screenSize|keyboardHidden"></activity>
	</application>
</manifest> 

We start with the "main" Activity file, DemoList.java.

public class DemoList extends ListActivity implements OnItemClickListener {
	private static final String TAG = "DemoList";
	private ListView listview;
	private ArrayAdapter<String> listAdapter;

	// Our data for plotting
	private final double[][] data = { 
			{ 1, 1.0 }, 
			{ 2.0, 4.0 }, 
			{ 3.0, 10.0 },
			{ 4, 2.0 }, 
			{ 5.0, 20 }, 
			{ 6.0, 4.0 }, 
			{ 7.0, 1.0 }, 
	};

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Log.d(TAG, "DemoList.onCreate()");

		// Set the View Layer
		setContentView(R.layout.data_listview);

		// Get the Default declared ListView @android:list
		listview = getListView();

		// List for click events to the ListView items
		listview.setOnItemClickListener(this);

		// Get the data to
		List<String> dataList = getDataStringList(data);

		// Create an Adapter to for viewing the ListView
		listAdapter = new ArrayAdapter<String>(this,
				android.R.layout.simple_list_item_1, dataList);

		// Bind the adapter to the ListView
		listview.setAdapter(listAdapter);

	}

	/**
	 * Convert the double[] to List<String> for display
	 * @param dataVals
	 * @return
	 */
	private List<String> getDataStringList(double[][] dataVals) {
		List<String> list = new ArrayList<String>();

		for (int i = 0; i < dataVals.length; i++) {
			String datum = "[" + String.valueOf(dataVals[i][0]) + ","
					+ String.valueOf(dataVals[i][1]) + "]";
			list.add(datum);
		}
		return list;
	}

	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
		Toast.makeText(this, "Orientation Change", Toast.LENGTH_SHORT).show();

		// Create an Intent to switch view to the Chart page view
		Intent intent = new Intent(this, DemoCharts.class);

		// The type "double[][]" does not go through as a SerializableExtra, so wrap in List.
		List<double[]> passedData = new ArrayList<double[]>();
		for (double[] dd : data) {
			passedData.add(dd);
		}
		
		// Pass parameters along to the next page
		intent.putExtra("param", (Serializable)passedData);
		

		// Start the activity
		startActivity(intent);
	}

	@Override
	public void onItemClick(AdapterView<?> parent, View view, int position,
			long duration) {
		// Upon clicking item in list pop a toast
		String msg = "#Item: " + String.valueOf(position) + " - "
				+ listAdapter.getItem(position);
		Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
	}
}

When you rotate the screen, the data is wrapped in a List and passed to the graphics routine, DemoCharts.java.

import net.droidsolutions.droidcharts.core.data.XYDataset;
import net.droidsolutions.droidcharts.core.data.xy.XYSeries;
import net.droidsolutions.droidcharts.core.data.xy.XYSeriesCollection;

public class DemoCharts extends Activity {
	private static final String TAG = "DemoCharts";
	private final String chartTitle = "My Daily Starbucks Allowance";
	private final String xLabel = "Week Day";
	private final String yLabel = "Allowance";

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Log.d(TAG, "DemoCharts.onCreate()");

		// Get the passed parameter values
		List<double[]> wrappedList = (List<double[]>) getIntent().getSerializableExtra("param");
		Log.d(TAG, "passed extra " + wrappedList + " is of type " + wrappedList.getClass());
		double[][] data = (double[][]) wrappedList.toArray(new double[wrappedList.size()][]);

		Log.d(TAG, "Data Param:= " + data);

		XYDataset dataset = createDataset("My Daily Starbucks Allowance", data);
		XYLineChartView graphView = new XYLineChartView(this, chartTitle,
				xLabel, yLabel, dataset);
		setContentView(graphView);
	}

	/**
	 * Creates an XYDataset from the raw data passed in.
	 */
	private XYDataset createDataset(String title, double[][] dataVals) {
		final XYSeries series1 = new XYSeries(title);
		for (double[] tuple : dataVals) {
			series1.add(tuple[0], tuple[1]);
		}

		// Create a collection to hold various data sets
		final XYSeriesCollection dataset = new XYSeriesCollection();
		dataset.addSeries(series1);
		return dataset;
	}

	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
		Toast.makeText(this, "Orientation Change", Toast.LENGTH_SHORT).show();

		// Let's get back to our DemoList view
		Intent intent = new Intent(this, DemoList.class);
		startActivity(intent);

		// Finish current Activity
		this.finish();
	}

This will pass control back to DemoList if the device is rotated again.

The actual graphing is done in XYLineChartView.java.

import net.droidsolutions.droidcharts.awt.Rectangle2D;
import net.droidsolutions.droidcharts.core.ChartFactory;
import net.droidsolutions.droidcharts.core.JFreeChart;
import net.droidsolutions.droidcharts.core.axis.NumberAxis;
import net.droidsolutions.droidcharts.core.data.XYDataset;
import net.droidsolutions.droidcharts.core.plot.PlotOrientation;
import net.droidsolutions.droidcharts.core.plot.XYPlot;
import net.droidsolutions.droidcharts.core.renderer.xy.XYLineAndShapeRenderer;

public class XYLineChartView extends View {

		private final String mChartTitle;
		private final String mXLabel;
		private final String mYLabel;
		private final XYDataset mDataSet;

		/** The view bounds. */
		private final Rect mRect = new Rect();

		/**
		 * Creates a new graphical view.
		 * 
		 * @param context
		 *          the context
		 * @param chart
		 *          the chart to be drawn
		 */
		public XYLineChartView(Context context, String chartTitle, String xLabel, String yLabel, XYDataset dataSet) {
				super(context);
				mChartTitle = chartTitle;
				mXLabel = xLabel;
				mYLabel = yLabel;
				mDataSet = dataSet;
		}
		
		public XYLineChartView(Context context) {
			this(context, "A Title", "An X label", "A Y Label", null);
		}

		@Override
		protected void onDraw(Canvas canvas) {

				super.onDraw(canvas);
				canvas.getClipBounds(mRect);

				// Get the passed in data set
				final XYDataset dataset = mDataSet;

				// Create the Chart
				final JFreeChart chart = createChart(dataset);

				// Draw it
				chart.draw(canvas, new Rectangle2D.Double(0, 0, mRect.width(), mRect.height()));
		}

		/**
		 * Creates a chart.
		 * 
		 * @param dataset
		 *          the data for the chart.
		 * 
		 * @return a chart.
		 */
		private JFreeChart createChart(final XYDataset dataset) {
				// create the chart...
				// (chart title, x-axis label, y-axis label,
				// dataset,orientation,orientation ,url)

				final JFreeChart chart = ChartFactory.createXYLineChart(mChartTitle, mXLabel, mYLabel, dataset, PlotOrientation.VERTICAL, true, true, false);

				Paint white = new Paint(Paint.ANTI_ALIAS_FLAG);
				white.setColor(Color.WHITE);

				Paint dkGray = new Paint(Paint.ANTI_ALIAS_FLAG);
				dkGray.setColor(Color.DKGRAY);

				Paint lightGray = new Paint(Paint.ANTI_ALIAS_FLAG);
				lightGray.setColor(Color.LTGRAY);
				lightGray.setStrokeWidth(10);

				// Set Chart Background color
				chart.setBackgroundPaint(white);

				final XYPlot plot = chart.getXYPlot();

				plot.setBackgroundPaint(dkGray);
				plot.setDomainGridlinePaint(lightGray);
				plot.setRangeGridlinePaint(lightGray);

				final XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
				renderer.setSeriesLinesVisible(0, true);
				plot.setRenderer(renderer);

				// change the auto tick unit selection to integer units only...
				final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
				rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
				// final NumberAxis domainAxis = (NumberAxis) plot.getDomainAxis();
				// domainAxis.set(CategoryLabelPositions.STANDARD);

				return chart;

			}
	}

See Also:

You cannot limit the orientation to "portrait" for the starting activity in the AndroidManifest, as that now appears to prevent rotation. So the onConfigurationChanged routines should be smarter, figuring out what the current orientation is, and choosing the "correct" view.

The graphics currently draws extra rectangle regions around each line segment; it's not clear what causes this.

The updated code shown here is in our Github repo; the author's original version may be found in the source download link listed.

Download:

The source code for this project can be downloaded from http://www.filefactory.com/file/b43d470/n/AndroidOrientationChanges.zip.

Download:

The source code for this project is in the Android Cookbook repository, http://github.com/IanDarwin/Android-Cookbook-Examples/,in the subdirectory OrientationChanges.