diff --git a/samples/ApiDemos/AndroidManifest.xml b/samples/ApiDemos/AndroidManifest.xml index 93df58ff4..4ebdf1636 100644 --- a/samples/ApiDemos/AndroidManifest.xml +++ b/samples/ApiDemos/AndroidManifest.xml @@ -996,6 +996,15 @@ + + + + + + + diff --git a/samples/ApiDemos/res/layout/motogp_stat_item.xml b/samples/ApiDemos/res/layout/motogp_stat_item.xml new file mode 100644 index 000000000..e09fd6786 --- /dev/null +++ b/samples/ApiDemos/res/layout/motogp_stat_item.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + diff --git a/samples/ApiDemos/res/menu/print_custom_content.xml b/samples/ApiDemos/res/menu/print_custom_content.xml new file mode 100644 index 000000000..ace8039e8 --- /dev/null +++ b/samples/ApiDemos/res/menu/print_custom_content.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/samples/ApiDemos/res/values/arrays.xml b/samples/ApiDemos/res/values/arrays.xml index b06220975..8f987ae43 100644 --- a/samples/ApiDemos/res/values/arrays.xml +++ b/samples/ApiDemos/res/values/arrays.xml @@ -154,4 +154,209 @@ No. Mmm... cheese. + + + + 2012 + 2011 + 2010 + 2009 + 2008 + 2007 + 2006 + 2005 + 2004 + 2003 + 2002 + 2001 + 2000 + 1999 + 1998 + 1997 + 1996 + 1995 + 1994 + 1993 + 1992 + 1991 + 1990 + 1989 + 1988 + 1987 + 1986 + 1985 + 1984 + 1983 + 1982 + 1981 + 1980 + 1979 + 1978 + 1977 + 1976 + 1975 + 1974 + 1973 + 1972 + 1971 + 1970 + 1969 + 1968 + 1967 + 1966 + 1965 + 1964 + 1963 + 1962 + 1961 + 1960 + 1959 + 1958 + 1957 + 1956 + 1955 + 1954 + 1953 + 1952 + 1951 + 1950 + 1949 + + + + + Jorge Lorenzo + Casey Stoner + Jorge Lorenzo + Valentino Rossi + Valentino Rossi + Casey Stoner + Nicky Hayden + Valentino Rossi + Valentino Rossi + Valentino Rossi + Valentino Rossi + Valentino Rossi + Kenny Roberts, Jr. + Àlex Crivillé + Michael Doohan + Michael Doohan + Michael Doohan + Michael Doohan + Michael Doohan + Kevin Schwantz + Wayne Rainey + Wayne Rainey + Wayne Rainey + Eddie Lawson + Eddie Lawson + Wayne Gardner + Eddie Lawson + Freddie Spencer + Eddie Lawson + Freddie Spencer + Franco Uncini + Marco Lucchinelli + Kenny Roberts + Kenny Roberts + Kenny Roberts + Barry Sheene + Barry Sheene + Giacomo Agostini + Phil Read + Phil Read + Giacomo Agostini + Giacomo Agostini + Giacomo Agostini + Giacomo Agostini + Giacomo Agostini + Giacomo Agostini + Giacomo Agostini + Mike Hailwood + Mike Hailwood + Mike Hailwood + Mike Hailwood + Gary Hocking + John Surtees + John Surtees + John Surtees + Libero Liberati + John Surtees + Geoff Duke + Geoff Duke + Geoff Duke + Umberto Masetti + Geoff Duke + Umberto Masetti + Leslie Graham + + + + + Yamaha + Honda + Yamaha + Yamaha + Yamaha + Ducati + Honda + Yamaha + Yamaha + Honda + Honda + Honda + Suzuki + Honda + Honda + Honda + Honda + Honda + Honda + Suzuki + Yamaha + Yamaha + Yamaha + Honda + Yamaha + Honda + Yamaha + Honda + Yamaha + Honda + Suzuki + Suzuki + Yamaha + Yamaha + Yamaha + Suzuki + Suzuki + Giacomo Agostini + Phil Read + Phil Read + Yamaha + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + MV Agusta + Gilera + MV Agusta + Gilera + Gilera + Gilera + Gilera + Norton + Gilera + AJS + + diff --git a/samples/ApiDemos/res/values/strings.xml b/samples/ApiDemos/res/values/strings.xml index 47445dde8..b9502e534 100644 --- a/samples/ApiDemos/res/values/strings.xml +++ b/samples/ApiDemos/res/values/strings.xml @@ -871,6 +871,13 @@ Toggle tab mode Remove all tabs + + + + + App/Print/Print Custom Layout + Print + diff --git a/samples/ApiDemos/src/com/example/android/apis/app/PrintCustomContent.java b/samples/ApiDemos/src/com/example/android/apis/app/PrintCustomContent.java new file mode 100644 index 000000000..0a7715c25 --- /dev/null +++ b/samples/ApiDemos/src/com/example/android/apis/app/PrintCustomContent.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2013 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.example.android.apis.app; + +import android.app.ListActivity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.pdf.PdfDocument.Page; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.CancellationSignal.OnCancelListener; +import android.os.ParcelFileDescriptor; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintDocumentInfo; +import android.print.PrintManager; +import android.print.pdf.PrintedPdfDocument; +import android.util.SparseIntArray; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.example.android.apis.R; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class demonstrates how to implement custom printing support. + *

+ * This activity shows the list of the MotoGP champions by year and + * brand. The print option in the overflow menu allows the user to + * print the content. The list list of items is laid out to such that + * it fits the options selected by the user from the UI such as page + * size. Hence, for different page sizes the printed content will have + * different page count. + *

+ *

+ * This sample demonstrates how to completely implement a {@link + * PrintDocumentAdapter} in which: + *

+ *

+ * + * @see PrintManager + * @see PrintDocumentAdapter + */ +public class PrintCustomContent extends ListActivity { + + private static final int MILS_IN_INCH = 1000; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setListAdapter(new MotoGpStatAdapter(loadMotoGpStats(), + getLayoutInflater())); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.print_custom_content, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.menu_print) { + print(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void print() { + PrintManager printManager = (PrintManager) getSystemService( + Context.PRINT_SERVICE); + + printManager.print("MotoGP stats", + new PrintDocumentAdapter() { + private int mRenderPageWidth; + private int mRenderPageHeight; + + private PrintAttributes mPrintAttributes; + private PrintDocumentInfo mDocumentInfo; + private Context mPrintContext; + + @Override + public void onLayout(final PrintAttributes oldAttributes, + final PrintAttributes newAttributes, + final CancellationSignal cancellationSignal, + final LayoutResultCallback callback, + final Bundle metadata) { + + // If we are already cancelled, don't do any work. + if (cancellationSignal.isCanceled()) { + callback.onLayoutCancelled(); + return; + } + + // Now we determined if the print attributes changed in a way that + // would change the layout and if so we will do a layout pass. + boolean layoutNeeded = false; + + final int density = Math.max(newAttributes.getResolution().getHorizontalDpi(), + newAttributes.getResolution().getVerticalDpi()); + + // Note that we are using the PrintedPdfDocument class which creates + // a PDF generating canvas whose size is in points (1/72") not screen + // pixels. Hence, this canvas is pretty small compared to the screen. + // The recommended way is to layout the content in the desired size, + // in this case as large as the printer can do, and set a translation + // to the PDF canvas to shrink in. Note that PDF is a vector format + // and you will not lose data during the transformation. + + // The content width is equal to the page width minus the margins times + // the horizontal printer density. This way we get the maximal number + // of pixels the printer can put horizontally. + final int marginLeft = (int) (density * (float) newAttributes.getMinMargins() + .getLeftMils() / MILS_IN_INCH); + final int marginRight = (int) (density * (float) newAttributes.getMinMargins() + .getRightMils() / MILS_IN_INCH); + final int contentWidth = (int) (density * (float) newAttributes.getMediaSize() + .getWidthMils() / MILS_IN_INCH) - marginLeft - marginRight; + if (mRenderPageWidth != contentWidth) { + mRenderPageWidth = contentWidth; + layoutNeeded = true; + } + + // The content height is equal to the page height minus the margins times + // the vertical printer resolution. This way we get the maximal number + // of pixels the printer can put vertically. + final int marginTop = (int) (density * (float) newAttributes.getMinMargins() + .getTopMils() / MILS_IN_INCH); + final int marginBottom = (int) (density * (float) newAttributes.getMinMargins() + .getBottomMils() / MILS_IN_INCH); + final int contentHeight = (int) (density * (float) newAttributes.getMediaSize() + .getHeightMils() / MILS_IN_INCH) - marginTop - marginBottom; + if (mRenderPageHeight != contentHeight) { + mRenderPageHeight = contentHeight; + layoutNeeded = true; + } + + // Create a context for resources at printer density. We will + // be inflating views to render them and would like them to use + // resources for a density the printer supports. + if (mPrintContext == null || mPrintContext.getResources() + .getConfiguration().densityDpi != density) { + Configuration configuration = new Configuration(); + configuration.densityDpi = density; + mPrintContext = createConfigurationContext( + configuration); + mPrintContext.setTheme(android.R.style.Theme_Holo_Light); + } + + // If no layout is needed that we did a layout at least once and + // the document info is not null, also the second argument is false + // to notify the system that the content did not change. This is + // important as if the system has some pages and the content didn't + // change the system will ask, the application to write them again. + if (!layoutNeeded) { + callback.onLayoutFinished(mDocumentInfo, false); + return; + } + + // For demonstration purposes we will do the layout off the main + // thread but for small content sizes like this one it is OK to do + // that on the main thread. + + // Store the data as we will layout off the main thread. + final List items = ((MotoGpStatAdapter) + getListAdapter()).cloneItems(); + + new AsyncTask() { + @Override + protected void onPreExecute() { + // First register for cancellation requests. + cancellationSignal.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel() { + cancel(true); + } + }); + // Stash the attributes as we will need them for rendering. + mPrintAttributes = newAttributes; + } + + @Override + protected PrintDocumentInfo doInBackground(Void... params) { + try { + // Create an adapter with the stats and an inflater + // to load resources for the printer density. + MotoGpStatAdapter adapter = new MotoGpStatAdapter(items, + (LayoutInflater) mPrintContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE)); + + int currentPage = 0; + int pageContentHeight = 0; + int viewType = -1; + View view = null; + LinearLayout dummyParent = new LinearLayout(mPrintContext); + dummyParent.setOrientation(LinearLayout.VERTICAL); + + final int itemCount = adapter.getCount(); + for (int i = 0; i < itemCount; i++) { + // Be nice and respond to cancellation. + if (isCancelled()) { + return null; + } + + // Get the next view. + final int nextViewType = adapter.getItemViewType(i); + if (viewType == nextViewType) { + view = adapter.getView(i, view, dummyParent); + } else { + view = adapter.getView(i, null, dummyParent); + } + viewType = nextViewType; + + // Measure the next view + measureView(view); + + // Add the height but if the view crosses the page + // boundary we will put it to the next page. + pageContentHeight += view.getMeasuredHeight(); + if (pageContentHeight > mRenderPageHeight) { + pageContentHeight = view.getMeasuredHeight(); + currentPage++; + } + } + + // Create a document info describing the result. + PrintDocumentInfo info = new PrintDocumentInfo + .Builder("MotoGP_stats.pdf") + .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT) + .setPageCount(currentPage + 1) + .build(); + + // We completed the layout as a result of print attributes + // change. Hence, if we are here the content changed for + // sure which is why we pass true as the second argument. + callback.onLayoutFinished(info, true); + return info; + } catch (Exception e) { + // An unexpected error, report that we failed and + // one may pass in a human readable localized text + // for what the error is if known. + callback.onLayoutFailed(null); + throw new RuntimeException(e); + } + } + + @Override + protected void onPostExecute(PrintDocumentInfo result) { + // Update the cached info to send it over if the next + // layout pass does not result in a content change. + mDocumentInfo = result; + } + + @Override + protected void onCancelled(PrintDocumentInfo result) { + // Task was cancelled, report that. + callback.onLayoutCancelled(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } + + @Override + public void onWrite(final PageRange[] pages, + final ParcelFileDescriptor destination, + final CancellationSignal cancellationSignal, + final WriteResultCallback callback) { + + // If we are already cancelled, don't do any work. + if (cancellationSignal.isCanceled()) { + callback.onWriteCancelled(); + return; + } + + // Store the data as we will layout off the main thread. + final List items = ((MotoGpStatAdapter) + getListAdapter()).cloneItems(); + + new AsyncTask() { + private final SparseIntArray mWrittenPages = new SparseIntArray(); + private final PrintedPdfDocument mPdfDocument = new PrintedPdfDocument( + PrintCustomContent.this, mPrintAttributes); + + @Override + protected void onPreExecute() { + // First register for cancellation requests. + cancellationSignal.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel() { + cancel(true); + } + }); + } + + @Override + protected Void doInBackground(Void... params) { + // Go over all the pages and write only the requested ones. + // Create an adapter with the stats and an inflater + // to load resources for the printer density. + MotoGpStatAdapter adapter = new MotoGpStatAdapter(items, + (LayoutInflater) mPrintContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE)); + + int currentPage = -1; + int pageContentHeight = 0; + int viewType = -1; + View view = null; + Page page = null; + LinearLayout dummyParent = new LinearLayout(mPrintContext); + dummyParent.setOrientation(LinearLayout.VERTICAL); + + // The content is laid out and rendered in screen pixels with + // the width and height of the paper size times the print + // density but the PDF canvas size is in points which are 1/72", + // so we will scale down the content. + final float scale = Math.min( + (float) mPdfDocument.getPageContentRect().width() + / mRenderPageWidth, + (float) mPdfDocument.getPageContentRect().height() + / mRenderPageHeight); + + final int itemCount = adapter.getCount(); + for (int i = 0; i < itemCount; i++) { + // Be nice and respond to cancellation. + if (isCancelled()) { + return null; + } + + // Get the next view. + final int nextViewType = adapter.getItemViewType(i); + if (viewType == nextViewType) { + view = adapter.getView(i, view, dummyParent); + } else { + view = adapter.getView(i, null, dummyParent); + } + viewType = nextViewType; + + // Measure the next view + measureView(view); + + // Add the height but if the view crosses the page + // boundary we will put it to the next one. + pageContentHeight += view.getHeight(); + if (currentPage < 0 || pageContentHeight > mRenderPageHeight) { + pageContentHeight = view.getHeight(); + currentPage++; + // Done with the current page - finish it. + if (page != null) { + mPdfDocument.finishPage(page); + } + // If the page is requested, render it. + if (containsPage(pages, currentPage)) { + page = mPdfDocument.startPage(currentPage); + page.getCanvas().scale(scale, scale); + // Keep track which pages are written. + mWrittenPages.append(mWrittenPages.size(), currentPage); + } else { + page = null; + } + } + + // If the current view is on a requested page, render it. + if (page != null) { + // Layout an render the content. + view.layout(0, 0, view.getMeasuredWidth(), + view.getMeasuredHeight()); + view.draw(page.getCanvas()); + // Move the canvas for the next view. + page.getCanvas().translate(0, view.getHeight()); + } + } + + // Done with the last page. + if (page != null) { + mPdfDocument.finishPage(page); + } + + // Write the data and return success or failure. + try { + mPdfDocument.writeTo(new FileOutputStream( + destination.getFileDescriptor())); + // Compute which page ranges were written based on + // the bookkeeping we maintained. + PageRange[] pageRanges = computeWrittenPageRanges(mWrittenPages); + callback.onWriteFinished(pageRanges); + } catch (IOException ioe) { + callback.onWriteFailed(null); + } finally { + mPdfDocument.close(); + } + + return null; + } + + @Override + protected void onCancelled(Void result) { + // Task was cancelled, report that. + callback.onWriteCancelled(); + mPdfDocument.close(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } + + private void measureView(View view) { + final int widthMeasureSpec = ViewGroup.getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mRenderPageWidth, + MeasureSpec.EXACTLY), 0, view.getLayoutParams().width); + final int heightMeasureSpec = ViewGroup.getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mRenderPageHeight, + MeasureSpec.EXACTLY), 0, view.getLayoutParams().height); + view.measure(widthMeasureSpec, heightMeasureSpec); + } + + private PageRange[] computeWrittenPageRanges(SparseIntArray writtenPages) { + List pageRanges = new ArrayList(); + + int start = -1; + int end = -1; + final int writtenPageCount = writtenPages.size(); + for (int i = 0; i < writtenPageCount; i++) { + if (start < 0) { + start = writtenPages.valueAt(i); + } + int oldEnd = end = start; + while (i < writtenPageCount && (end - oldEnd) <= 1) { + oldEnd = end; + end = writtenPages.valueAt(i); + i++; + } + PageRange pageRange = new PageRange(start, end); + pageRanges.add(pageRange); + start = end = -1; + } + + PageRange[] pageRangesArray = new PageRange[pageRanges.size()]; + pageRanges.toArray(pageRangesArray); + return pageRangesArray; + } + + private boolean containsPage(PageRange[] pageRanges, int page) { + final int pageRangeCount = pageRanges.length; + for (int i = 0; i < pageRangeCount; i++) { + if (pageRanges[i].getStart() <= page + && pageRanges[i].getEnd() >= page) { + return true; + } + } + return false; + } + }, null); + } + + private List loadMotoGpStats() { + String[] years = getResources().getStringArray(R.array.motogp_years); + String[] champions = getResources().getStringArray(R.array.motogp_champions); + String[] constructors = getResources().getStringArray(R.array.motogp_constructors); + + List items = new ArrayList(); + + final int itemCount = years.length; + for (int i = 0; i < itemCount; i++) { + MotoGpStatItem item = new MotoGpStatItem(); + item.year = years[i]; + item.champion = champions[i]; + item.constructor = constructors[i]; + items.add(item); + } + + return items; + } + + private static final class MotoGpStatItem { + String year; + String champion; + String constructor; + } + + private class MotoGpStatAdapter extends BaseAdapter { + private final List mItems; + private final LayoutInflater mInflater; + + public MotoGpStatAdapter(List items, LayoutInflater inflater) { + mItems = items; + mInflater = inflater; + } + + public List cloneItems() { + return new ArrayList(mItems); + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.motogp_stat_item, parent, false); + } + + MotoGpStatItem item = (MotoGpStatItem) getItem(position); + + TextView yearView = (TextView) convertView.findViewById(R.id.year); + yearView.setText(item.year); + + TextView championView = (TextView) convertView.findViewById(R.id.champion); + championView.setText(item.champion); + + TextView constructorView = (TextView) convertView.findViewById(R.id.constructor); + constructorView.setText(item.constructor); + + return convertView; + } + } +}