From ced88667b08756f009f6c0c15f2c0c2cd1dace8b Mon Sep 17 00:00:00 2001 From: Chris Wren Date: Thu, 16 Aug 2012 17:33:34 -0400 Subject: Example Dream: photos An example screensaver that shows local photos. Change-Id: I2997c4f8ab35777d96df31a46f2f55044e3114c2 --- Android.mk | 19 + AndroidManifest.xml | 22 + res/anim/slideshow_in.xml | 5 + res/anim/slideshow_out.xml | 7 + res/drawable-hdpi/icon.png | Bin 0 -> 4713 bytes res/drawable-ldpi/icon.png | Bin 0 -> 1707 bytes res/drawable-mdpi/icon.png | Bin 0 -> 2850 bytes res/drawable-nodpi/frame.9.png | Bin 0 -> 647 bytes res/drawable-nodpi/photo_039_002.jpg | Bin 0 -> 314891 bytes res/drawable-nodpi/photo_044_002.jpg | Bin 0 -> 252272 bytes res/drawable-nodpi/photo_059_003.jpg | Bin 0 -> 270403 bytes res/drawable-nodpi/photo_070_004.jpg | Bin 0 -> 324820 bytes res/drawable-nodpi/photo_072_001.jpg | Bin 0 -> 364562 bytes res/drawable-nodpi/photo_077_002.jpg | Bin 0 -> 330051 bytes res/drawable-nodpi/photo_098_002.jpg | Bin 0 -> 363659 bytes res/drawable-nodpi/photo_119_003.jpg | Bin 0 -> 483796 bytes res/drawable-nodpi/photo_119_004.jpg | Bin 0 -> 376106 bytes res/drawable-nodpi/photo_126_001.jpg | Bin 0 -> 466321 bytes res/drawable-nodpi/photo_147_002.jpg | Bin 0 -> 340770 bytes res/drawable-nodpi/photo_175_004.jpg | Bin 0 -> 229233 bytes res/drawable-xhdpi/icon.png | Bin 0 -> 6817 bytes res/layout/photo.xml | 22 + res/mipmap-hdpi/icon.png | Bin 0 -> 4713 bytes res/mipmap-mdpi/icon.png | Bin 0 -> 2850 bytes res/values-sw600dp/config.xml | 26 ++ res/values-sw800dp/config.xml | 26 ++ res/values/colors.xml | 18 + res/values/config.xml | 33 ++ res/values/dimen.xml | 18 + res/values/ids.xml | 20 + res/values/strings.xml | 19 + src/com/android/dreams/phototable/LocalSource.java | 193 ++++++++ src/com/android/dreams/phototable/PhotoTable.java | 514 +++++++++++++++++++++ 33 files changed, 942 insertions(+) create mode 100644 Android.mk create mode 100644 AndroidManifest.xml create mode 100644 res/anim/slideshow_in.xml create mode 100644 res/anim/slideshow_out.xml create mode 100644 res/drawable-hdpi/icon.png create mode 100644 res/drawable-ldpi/icon.png create mode 100644 res/drawable-mdpi/icon.png create mode 100644 res/drawable-nodpi/frame.9.png create mode 100644 res/drawable-nodpi/photo_039_002.jpg create mode 100644 res/drawable-nodpi/photo_044_002.jpg create mode 100644 res/drawable-nodpi/photo_059_003.jpg create mode 100644 res/drawable-nodpi/photo_070_004.jpg create mode 100644 res/drawable-nodpi/photo_072_001.jpg create mode 100644 res/drawable-nodpi/photo_077_002.jpg create mode 100644 res/drawable-nodpi/photo_098_002.jpg create mode 100644 res/drawable-nodpi/photo_119_003.jpg create mode 100644 res/drawable-nodpi/photo_119_004.jpg create mode 100644 res/drawable-nodpi/photo_126_001.jpg create mode 100644 res/drawable-nodpi/photo_147_002.jpg create mode 100644 res/drawable-nodpi/photo_175_004.jpg create mode 100644 res/drawable-xhdpi/icon.png create mode 100644 res/layout/photo.xml create mode 100644 res/mipmap-hdpi/icon.png create mode 100644 res/mipmap-mdpi/icon.png create mode 100644 res/values-sw600dp/config.xml create mode 100644 res/values-sw800dp/config.xml create mode 100644 res/values/colors.xml create mode 100644 res/values/config.xml create mode 100644 res/values/dimen.xml create mode 100644 res/values/ids.xml create mode 100644 res/values/strings.xml create mode 100644 src/com/android/dreams/phototable/LocalSource.java create mode 100644 src/com/android/dreams/phototable/PhotoTable.java diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..40ee416 --- /dev/null +++ b/Android.mk @@ -0,0 +1,19 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13 \ + google-common + +LOCAL_PACKAGE_NAME := PhotoTable + +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) + +# Use the following include to make our test apk. +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..114a8ef --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/res/anim/slideshow_in.xml b/res/anim/slideshow_in.xml new file mode 100644 index 0000000..225b250 --- /dev/null +++ b/res/anim/slideshow_in.xml @@ -0,0 +1,5 @@ + + diff --git a/res/anim/slideshow_out.xml b/res/anim/slideshow_out.xml new file mode 100644 index 0000000..5438e17 --- /dev/null +++ b/res/anim/slideshow_out.xml @@ -0,0 +1,7 @@ + + diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png new file mode 100644 index 0000000..b57ffa4 Binary files /dev/null and b/res/drawable-hdpi/icon.png differ diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png new file mode 100644 index 0000000..d712819 Binary files /dev/null and b/res/drawable-ldpi/icon.png differ diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..5cd2f30 Binary files /dev/null and b/res/drawable-mdpi/icon.png differ diff --git a/res/drawable-nodpi/frame.9.png b/res/drawable-nodpi/frame.9.png new file mode 100644 index 0000000..5262ccf Binary files /dev/null and b/res/drawable-nodpi/frame.9.png differ diff --git a/res/drawable-nodpi/photo_039_002.jpg b/res/drawable-nodpi/photo_039_002.jpg new file mode 100644 index 0000000..f2fa084 Binary files /dev/null and b/res/drawable-nodpi/photo_039_002.jpg differ diff --git a/res/drawable-nodpi/photo_044_002.jpg b/res/drawable-nodpi/photo_044_002.jpg new file mode 100644 index 0000000..a05ea32 Binary files /dev/null and b/res/drawable-nodpi/photo_044_002.jpg differ diff --git a/res/drawable-nodpi/photo_059_003.jpg b/res/drawable-nodpi/photo_059_003.jpg new file mode 100644 index 0000000..6689a31 Binary files /dev/null and b/res/drawable-nodpi/photo_059_003.jpg differ diff --git a/res/drawable-nodpi/photo_070_004.jpg b/res/drawable-nodpi/photo_070_004.jpg new file mode 100644 index 0000000..3821443 Binary files /dev/null and b/res/drawable-nodpi/photo_070_004.jpg differ diff --git a/res/drawable-nodpi/photo_072_001.jpg b/res/drawable-nodpi/photo_072_001.jpg new file mode 100644 index 0000000..6ef496a Binary files /dev/null and b/res/drawable-nodpi/photo_072_001.jpg differ diff --git a/res/drawable-nodpi/photo_077_002.jpg b/res/drawable-nodpi/photo_077_002.jpg new file mode 100644 index 0000000..c3d9386 Binary files /dev/null and b/res/drawable-nodpi/photo_077_002.jpg differ diff --git a/res/drawable-nodpi/photo_098_002.jpg b/res/drawable-nodpi/photo_098_002.jpg new file mode 100644 index 0000000..3012636 Binary files /dev/null and b/res/drawable-nodpi/photo_098_002.jpg differ diff --git a/res/drawable-nodpi/photo_119_003.jpg b/res/drawable-nodpi/photo_119_003.jpg new file mode 100644 index 0000000..7d468e5 Binary files /dev/null and b/res/drawable-nodpi/photo_119_003.jpg differ diff --git a/res/drawable-nodpi/photo_119_004.jpg b/res/drawable-nodpi/photo_119_004.jpg new file mode 100644 index 0000000..a1dc8cd Binary files /dev/null and b/res/drawable-nodpi/photo_119_004.jpg differ diff --git a/res/drawable-nodpi/photo_126_001.jpg b/res/drawable-nodpi/photo_126_001.jpg new file mode 100644 index 0000000..072006b Binary files /dev/null and b/res/drawable-nodpi/photo_126_001.jpg differ diff --git a/res/drawable-nodpi/photo_147_002.jpg b/res/drawable-nodpi/photo_147_002.jpg new file mode 100644 index 0000000..3ee61b2 Binary files /dev/null and b/res/drawable-nodpi/photo_147_002.jpg differ diff --git a/res/drawable-nodpi/photo_175_004.jpg b/res/drawable-nodpi/photo_175_004.jpg new file mode 100644 index 0000000..5ff2ad2 Binary files /dev/null and b/res/drawable-nodpi/photo_175_004.jpg differ diff --git a/res/drawable-xhdpi/icon.png b/res/drawable-xhdpi/icon.png new file mode 100644 index 0000000..5f94c42 Binary files /dev/null and b/res/drawable-xhdpi/icon.png differ diff --git a/res/layout/photo.xml b/res/layout/photo.xml new file mode 100644 index 0000000..988dbe4 --- /dev/null +++ b/res/layout/photo.xml @@ -0,0 +1,22 @@ + + + + diff --git a/res/mipmap-hdpi/icon.png b/res/mipmap-hdpi/icon.png new file mode 100644 index 0000000..b57ffa4 Binary files /dev/null and b/res/mipmap-hdpi/icon.png differ diff --git a/res/mipmap-mdpi/icon.png b/res/mipmap-mdpi/icon.png new file mode 100644 index 0000000..5cd2f30 Binary files /dev/null and b/res/mipmap-mdpi/icon.png differ diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml new file mode 100644 index 0000000..74122db --- /dev/null +++ b/res/values-sw600dp/config.xml @@ -0,0 +1,26 @@ + + + + + 8 + + + 8 + + + 500000 + + diff --git a/res/values-sw800dp/config.xml b/res/values-sw800dp/config.xml new file mode 100644 index 0000000..5288f98 --- /dev/null +++ b/res/values-sw800dp/config.xml @@ -0,0 +1,26 @@ + + + + + 3 + + + 3 + + + 500000 + + diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 0000000..9d5864e --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,18 @@ + + + + #ff444444 + diff --git a/res/values/config.xml b/res/values/config.xml new file mode 100644 index 0000000..f1c9fcc --- /dev/null +++ b/res/values/config.xml @@ -0,0 +1,33 @@ + + + + + 30000 + + + 10 + + + 10 + + + 1000000 + + + 1000 + + diff --git a/res/values/dimen.xml b/res/values/dimen.xml new file mode 100644 index 0000000..7e070df --- /dev/null +++ b/res/values/dimen.xml @@ -0,0 +1,18 @@ + + + + 4px + diff --git a/res/values/ids.xml b/res/values/ids.xml new file mode 100644 index 0000000..3f65a0c --- /dev/null +++ b/res/values/ids.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..94e2f9c --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + Photo Table Screensaver + Photo Table + diff --git a/src/com/android/dreams/phototable/LocalSource.java b/src/com/android/dreams/phototable/LocalSource.java new file mode 100644 index 0000000..1414567 --- /dev/null +++ b/src/com/android/dreams/phototable/LocalSource.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2012 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.dreams.phototable; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.provider.MediaStore; +import android.util.Log; + +import java.io.FileInputStream; +import java.io.BufferedInputStream; +import java.util.Collections; +import java.util.LinkedList; +import java.util.Random; + +/** + * Picks a random image from the local store. + */ +public class LocalSource { + private static final String TAG = "PhotoTable.LocalSource"; + static final boolean DEBUG = false; + + public static class ImageData { + public String path; + public int orientation; + } + + private final ContentResolver mResolver; + private final Context mContext; + private final LinkedList mImageQueue; + private final float mImageRatio; + private final int mMaxQueueSize; + private final Random mRNG; + private int mNextPosition; + + public LocalSource(Context context) { + mContext = context; + mResolver = mContext.getContentResolver(); + mNextPosition = -1; + mImageQueue = new LinkedList(); + mImageRatio = context.getResources().getInteger(R.integer.image_ratio) / 1000000f; + mMaxQueueSize = context.getResources().getInteger(R.integer.image_queue_size); + mRNG = new Random(); + fillQueue(); + } + + private void fillQueue() { + log("filling queue"); + String[] projection = {MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION}; + Cursor cursor = mResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, null, null, null); + if (cursor != null) { + if (cursor.getCount() > mMaxQueueSize && mNextPosition == -1) { + mNextPosition = mRNG.nextInt() % (cursor.getCount() - mMaxQueueSize); + } + if (mNextPosition == -1) { + mNextPosition = 0; + } + cursor.moveToPosition(mNextPosition); + + int dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA); + int orientationIndex = cursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION); + if (dataIndex < 0) { + log("can't find the DATA column!"); + } else { + while (mImageQueue.size() < mMaxQueueSize && !cursor.isAfterLast()) { + ImageData data = new ImageData(); + data.path = cursor.getString(dataIndex); + data.orientation = cursor.getInt(orientationIndex); + mImageQueue.offer(data); + if (cursor.moveToNext()) { + mNextPosition++; + } + } + if (cursor.isAfterLast()) { + mNextPosition = 0; + } + } + cursor.close(); + } + Collections.shuffle(mImageQueue); + log("queue contains: " + mImageQueue.size() + " items."); + } + + public Bitmap next(BitmapFactory.Options options, int longSide, int shortSide) { + log("decoding a local resource to " + longSide + ", " + shortSide); + Bitmap image = null; + + if (mImageQueue.isEmpty()) { + fillQueue(); + } + + if (!mImageQueue.isEmpty()) { + ImageData data = mImageQueue.poll(); + + FileInputStream fis = null; + try { + log("decoding:" + data.path); + fis = new FileInputStream(data.path); + options.inJustDecodeBounds = true; + options.inSampleSize = 1; + BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options); + int rawLongSide = Math.max(options.outWidth, options.outHeight); + int rawShortSide = Math.max(options.outWidth, options.outHeight); + log("I see bounds of " + rawLongSide + ", " + rawShortSide); + + float ratio = Math.min((float) longSide / (float) rawLongSide, + (float) shortSide / (float) rawShortSide); + while (ratio < 0.5) { + options.inSampleSize *= 2; + ratio *= 2; + } + + log("decoding with inSampleSize " + options.inSampleSize); + options.inJustDecodeBounds = false; + image = BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options); + rawLongSide = Math.max(options.outWidth, options.outHeight); + rawShortSide = Math.max(options.outWidth, options.outHeight); + ratio = Math.min((float) longSide / (float) rawLongSide, + (float) shortSide / (float) rawShortSide); + + if (ratio < 1.0f) { + log("still too big, scaling down by " + ratio); + options.outWidth = (int) (ratio * options.outWidth); + options.outHeight = (int) (ratio * options.outHeight); + image = Bitmap.createScaledBitmap(image, + options.outWidth, options.outHeight, + true); + } + + if (data.orientation != 0) { + log("rotated by " + data.orientation + ": fixing"); + if (data.orientation == 90 || data.orientation == 270) { + int tmp = options.outWidth; + options.outWidth = options.outHeight; + options.outHeight = tmp; + } + Matrix matrix = new Matrix(); + matrix.setRotate(- data.orientation, + (float) image.getWidth() / 2, + (float) image.getHeight() / 2); + image = Bitmap.createBitmap(image, 0, 0, + options.outWidth, options.outHeight, + matrix, true); + } + + log("returning bitmap sized to " + image.getWidth() + ", " + image.getHeight()); + } catch (Exception ex) { + log(ex.toString()); + return null; + } finally { + try { + if (fis != null) { + fis.close(); + } + } catch (Throwable t) { + log("close fail: " + t.toString()); + } + } + } else { + log("device has no local images."); + } + + return image; + } + + public void setSeed(long seed) { + mRNG.setSeed(seed); + } + + private void log(String message) { + if (DEBUG) { + Log.i(TAG, message); + } + } +} diff --git a/src/com/android/dreams/phototable/PhotoTable.java b/src/com/android/dreams/phototable/PhotoTable.java new file mode 100644 index 0000000..a8b36ea --- /dev/null +++ b/src/com/android/dreams/phototable/PhotoTable.java @@ -0,0 +1,514 @@ +/* + * Copyright (C) 2012 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.dreams.phototable; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.service.dreams.Dream; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.PointF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.AsyncTask; +import android.os.PowerManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Random; + +/** + * Example interactive screen saver. + */ +public class PhotoTable extends Dream { + private static final String TAG = "PhotoTable"; + private static final boolean DEBUG = false; + private static final int[] PHOTOS = {R.drawable.photo_044_002, + R.drawable.photo_039_002, + R.drawable.photo_059_003, + R.drawable.photo_070_004, + R.drawable.photo_072_001, + R.drawable.photo_077_002, + R.drawable.photo_098_002, + R.drawable.photo_119_003, + R.drawable.photo_119_004, + R.drawable.photo_126_001, + R.drawable.photo_147_002, + R.drawable.photo_175_004 + }; + + private Table mTable; + + public static class Table extends FrameLayout { + class Launcher implements Runnable { + private final Table mTable; + public Launcher(Table table) { + mTable = table; + } + + @Override + public void run() { + mTable.launch(); + } + } + + private static final long MAX_SELECTION_TIME = 10000L; + private static Random sRNG = new Random(); + + private final Launcher mLauncher; + private boolean mStarted; + private LinkedList mOnTable; + private Dream mDream; + private int mDropPeriod; + private float mImageRatio; + private int mTableCapacity; + private int mInset; + private BitmapFactory.Options mOptions; + private int mLongSide; + private int mShortSide; + private int mWidth; + private int mHeight; + private View mSelected; + private long mSelectedTime; + private LocalSource mLocalSource; + private Resources mResources; + private PointF[] mDropZone; + + public Table(Dream dream, AttributeSet as) { + super(dream, as); + mDream = dream; + mResources = getResources(); + setBackgroundColor(mResources.getColor(R.color.tabletop)); + mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset); + mDropPeriod = mResources.getInteger(R.integer.drop_period); + mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f; + mTableCapacity = mResources.getInteger(R.integer.table_capacity); + mOnTable = new LinkedList(); + mOptions = new BitmapFactory.Options(); + mOptions.inTempStorage = new byte[32768]; + mLocalSource = new LocalSource(getContext()); + mLauncher = new Launcher(this); + mStarted = false; + } + + public boolean hasSelection() { + return mSelected != null; + } + + public View getSelected() { + return mSelected; + } + + public void clearSelection() { + mSelected = null; + } + + public void setSelection(View selected) { + assert(selected != null); + mSelected = selected; + mSelectedTime = System.currentTimeMillis(); + } + + static float lerp(float a, float b, float f) { + return (b-a)*f + a; + } + + static float randfrange(float a, float b) { + return lerp(a, b, sRNG.nextFloat()); + } + + static PointF randFromCurve(float t, PointF[] v) { + PointF p = new PointF(); + if (v.length == 4 && t >= 0f && t <= 1f) { + float a = (float) Math.pow(1f-t, 3f); + float b = (float) Math.pow(1f-t, 2f) * t; + float c = (1f-t) * (float) Math.pow(t, 2f); + float d = (float) Math.pow(t, 3f); + + p.x = a * v[0].x + 3 * b * v[1].x + 3 * c * v[2].x + d * v[3].x; + p.y = a * v[0].y + 3 * b * v[1].y + 3 * c * v[2].y + d * v[3].y; + } + return p; + } + + private static PointF randInCenter(float i, float j, int width, int height) { + log("randInCenter (" + i + ", " + j + ", " + width + ", " + height + ")"); + PointF p = new PointF(); + p.x = 0.5f * width + 0.15f * width * i; + p.y = 0.5f * height + 0.15f * height * j; + log("randInCenter returning " + p.x + "," + p.y); + return p; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + if (hasSelection()) { + dropOnTable(getSelected()); + clearSelection(); + } else { + mDream.finish(); + } + return true; + } + return false; + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + log("onLayout (" + left + ", " + top + ", " + right + ", " + bottom + ")"); + + mHeight = bottom - top; + mWidth = right - left; + + if (mDropZone == null) { + mDropZone = new PointF[4]; + mDropZone[0] = new PointF(); + mDropZone[1] = new PointF(); + mDropZone[2] = new PointF(); + mDropZone[3] = new PointF(); + } + mDropZone[0].x = 0f; + mDropZone[0].y = 0.75f * mHeight; + mDropZone[1].x = 0f; + mDropZone[1].y = 0f; + mDropZone[2].x = 0f; + mDropZone[2].y = 0f; + mDropZone[3].x = 0.75f * mWidth; + mDropZone[3].y = 0f; + + mLongSide = Math.max(mWidth, mHeight); + mShortSide = Math.min(mWidth, mHeight); + + start(); + } + + @Override + public boolean isOpaque() { + return true; + } + + @SuppressWarnings("deprecation") + private void launch() { + scheduleNext(); + + log("launching"); + setSystemUiVisibility(View.STATUS_BAR_HIDDEN); + if (hasSelection() && + (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) { + dropOnTable(getSelected()); + clearSelection(); + } else { + log("inflate it"); + AsyncTask task = new AsyncTask() { + @Override + public View doInBackground(Void... unused) { + log("load a new photo"); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View photo = inflater.inflate(R.layout.photo, null); + ImageView image = (ImageView) photo; + Drawable[] layers = new Drawable[2]; + Bitmap decodedPhoto = null; + decodedPhoto = mLocalSource.next(mOptions, mLongSide, mShortSide); + if (decodedPhoto == null) { + decodedPhoto = nextStockPhoto(mOptions, mLongSide, mShortSide); + } + int photoWidth = mOptions.outWidth; + int photoHeight = mOptions.outHeight; + if (mOptions.outWidth <= 0 || mOptions.outHeight <= 0) { + photo = null; + } else { + layers[0] = new BitmapDrawable(mResources, decodedPhoto); + layers[1] = mResources.getDrawable(R.drawable.frame); + LayerDrawable layerList = new LayerDrawable(layers); + layerList.setLayerInset(0, mInset, mInset, mInset, mInset); + image.setImageDrawable(layerList); + + photo.setTag(R.id.photo_width, new Integer(photoWidth)); + photo.setTag(R.id.photo_height, new Integer(photoHeight)); + } + + return photo; + } + + @Override + public void onPostExecute(View photo) { + if (photo != null) { + addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT)); + if (hasSelection()) { + bringChildToFront(getSelected()); + } + int width = ((Integer) photo.getTag(R.id.photo_width)).intValue(); + int height = ((Integer) photo.getTag(R.id.photo_height)).intValue(); + + log("drop it"); + throwOnTable(photo); + } + } + }; + task.execute(); + } + } + + private Bitmap nextStockPhoto(BitmapFactory.Options options, int longSide, int shortSide) { + log("decoding a local resource to " + longSide + ", " + shortSide); + int photo = PHOTOS[Math.abs(sRNG.nextInt() % PHOTOS.length)]; + + options.inJustDecodeBounds = true; + options.inSampleSize = 1; + BitmapFactory.decodeResource(mResources, photo, options); + int rawLongSide = Math.max(options.outWidth, options.outHeight); + int rawShortSide = Math.max(options.outWidth, options.outHeight); + log("I see bounds of " + rawLongSide + ", " + rawShortSide); + float ratio = Math.min((float) longSide / (float) rawLongSide, + (float) shortSide / (float) rawShortSide); + while (ratio < 0.5) { + options.inSampleSize *= 2; + ratio *= 2; + } + log("decoding with inSampleSize " + options.inSampleSize); + options.inJustDecodeBounds = false; + Bitmap bitmap = BitmapFactory.decodeResource(mResources, photo, options); + rawLongSide = Math.max(options.outWidth, options.outHeight); + rawShortSide = Math.max(options.outWidth, options.outHeight); + ratio = Math.min((float) longSide / (float) rawLongSide, + (float) shortSide / (float) rawShortSide); + + if (ratio < 1.0f) { + log("still too big, scaling down by " + ratio); + int photoWidth = (int) (ratio * options.outWidth); + int photoHeight = (int) (ratio * options.outHeight); + bitmap = Bitmap.createScaledBitmap(bitmap, photoWidth, photoHeight, true); + } + + log("returning bitmap sized to " + bitmap.getWidth() + ", " + bitmap.getHeight()); + return bitmap; + } + + private void fadeAway(final View photo) { + // fade out of view + photo.animate().cancel(); + photo.animate() + .withLayer() + .alpha(0f) + .setDuration(1000) + .withEndAction(new Runnable() { + @Override + public void run() { + removeView(photo); + recycle(photo); + } + }); + } + + private void throwOnTable(final View photo) { + mOnTable.offer(photo); + log("start offscreen"); + int width = ((Integer) photo.getTag(R.id.photo_width)); + int height = ((Integer) photo.getTag(R.id.photo_height)); + photo.setRotation(-100.0f); + photo.setX(-width); + photo.setY(-height); + dropOnTable(photo); + } + + private void dropOnTable(final View photo) { + float angle = randfrange(-60, 60f); + PointF p = randInCenter((float) sRNG.nextGaussian(), (float) sRNG.nextGaussian(), + mWidth, mHeight); + float x = p.x; + float y = p.y; + + log("drop it at " + x + ", " + y); + + float x0 = photo.getX(); + float y0 = photo.getY(); + float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue(); + float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue(); + + x -= width / 2f; + y -= height / 2f; + log("fixed offset is " + x + ", " + y); + + float dx = x - x0; + float dy = y - y0; + + float dist = (float) (Math.sqrt(dx * dx + dy * dy)); + int duration = (int) (1000f * dist / 400f); + duration = Math.max(duration, 1000); + + log("animate it"); + // toss onto table + photo.animate() + .withLayer() + .scaleX(0.5f) + .scaleY(0.5f) + .rotation(angle) + .x(x) + .y(y) + .setDuration(duration) + .withEndAction(new Runnable() { + @Override + public void run() { + while (mOnTable.size() > mTableCapacity) { + fadeAway(mOnTable.poll()); + } + } + }); + + photo.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View target) { + if (hasSelection()) { + dropOnTable(getSelected()); + clearSelection(); + } + target.animate().cancel(); + bringChildToFront(target); + setSelection(target); + pickUp(getSelected()); + } + }); + } + + private void pickUp(final View photo) { + float photoWidth = photo.getWidth(); + float photoHeight = photo.getHeight(); + + float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth); + + log("target it"); + float x = (getWidth() - photoWidth) / 2f; + float y = (getHeight() - photoHeight) / 2f; + + float x0 = photo.getX(); + float y0 = photo.getY(); + float dx = x - x0; + float dy = y - y0; + + float dist = (float) (Math.sqrt(dx * dx + dy * dy)); + int duration = (int) (1000f * dist / 1000f); + duration = Math.max(duration, 500); + + log("animate it"); + // toss onto table + photo.animate() + .withLayer() + .rotation(0f) + .scaleX(scale) + .scaleY(scale) + .x(x) + .y(y) + .setDuration(duration) + .setInterpolator(new DecelerateInterpolator()) + .withEndAction(new Runnable() { + @Override + public void run() { + log("endtimes: " + photo.getX()); + } + }); + + photo.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View target) { + if (getSelected() == photo) { + dropOnTable(photo); + clearSelection(); + } + } + }); + } + + private static void log(String message) { + if (DEBUG) { + Log.i(TAG, message); + } + } + + private void recycle(View photo) { + ImageView image = (ImageView) photo; + LayerDrawable layers = (LayerDrawable) image.getDrawable(); + BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0); + bitmap.getBitmap().recycle(); + } + + public void recycleAll() { + new AsyncTask() { + @Override + public void onPreExecute() { + for (View photo: mOnTable) { + removeView(photo); + } + } + + @Override + public Void doInBackground(Void... unused) { + while (!mOnTable.isEmpty()) { + recycle(mOnTable.poll()); + } + return null; + } + }.execute(); + } + + public void start() { + if (!mStarted) { + log("kick it"); + mStarted = true; + for (int i = 0; i < mResources.getInteger(R.integer.initial_drop); i++) { + launch(); + } + } + } + + public void scheduleNext() { + removeCallbacks(mLauncher); + postDelayed(mLauncher, mDropPeriod); + } + } + + @Override + public void onStart() { + super.onStart(); + setInteractive(true); + mTable = new Table(this, null); + setContentView(mTable); + lightsOut(); + } + + @Override + public void onDestroy() { + mTable.recycleAll(); + super.onDestroy(); + } +} -- cgit v1.2.3