| /* |
| * 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.service.dreams.DreamService; |
| import android.content.Context; |
| import android.content.SharedPreferences; |
| 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.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.Interpolator; |
| import android.widget.FrameLayout; |
| import android.widget.FrameLayout.LayoutParams; |
| import android.widget.ImageView; |
| |
| import java.util.LinkedList; |
| import java.util.Random; |
| |
| /** |
| * A surface where photos sit. |
| */ |
| public class PhotoTable extends FrameLayout { |
| private static final String TAG = "PhotoTable"; |
| private static final boolean DEBUG = false; |
| |
| class Launcher implements Runnable { |
| private final PhotoTable mTable; |
| public Launcher(PhotoTable table) { |
| mTable = table; |
| } |
| |
| @Override |
| public void run() { |
| mTable.scheduleNext(mDropPeriod); |
| mTable.launch(); |
| } |
| } |
| |
| private static final long MAX_SELECTION_TIME = 10000L; |
| private static Random sRNG = new Random(); |
| |
| private final Launcher mLauncher; |
| private final LinkedList<View> mOnTable; |
| private final int mDropPeriod; |
| private final int mFastDropPeriod; |
| private final int mNowDropDelay; |
| private final float mImageRatio; |
| private final float mTableRatio; |
| private final float mImageRotationLimit; |
| private final float mThrowRotation; |
| private final float mThrowSpeed; |
| private final boolean mTapToExit; |
| private final int mTableCapacity; |
| private final int mRedealCount; |
| private final int mInset; |
| private final PhotoSourcePlexor mPhotoSource; |
| private final Resources mResources; |
| private final Interpolator mThrowInterpolator; |
| private final Interpolator mDropInterpolator; |
| private DreamService mDream; |
| private PhotoLaunchTask mPhotoLaunchTask; |
| private boolean mStarted; |
| private boolean mIsLandscape; |
| private int mLongSide; |
| private int mShortSide; |
| private int mWidth; |
| private int mHeight; |
| private View mSelected; |
| private long mSelectedTime; |
| |
| public PhotoTable(Context context, AttributeSet as) { |
| super(context, as); |
| mResources = getResources(); |
| mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset); |
| mDropPeriod = mResources.getInteger(R.integer.table_drop_period); |
| mFastDropPeriod = mResources.getInteger(R.integer.fast_drop); |
| mNowDropDelay = mResources.getInteger(R.integer.now_drop); |
| mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f; |
| mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f; |
| mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation); |
| mThrowSpeed = mResources.getDimension(R.dimen.image_throw_speed); |
| mThrowRotation = (float) mResources.getInteger(R.integer.image_throw_rotatioan); |
| mTableCapacity = mResources.getInteger(R.integer.table_capacity); |
| mRedealCount = mResources.getInteger(R.integer.redeal_count); |
| mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit); |
| mThrowInterpolator = new SoftLandingInterpolator( |
| mResources.getInteger(R.integer.soft_landing_time) / 1000000f, |
| mResources.getInteger(R.integer.soft_landing_distance) / 1000000f); |
| mDropInterpolator = new DecelerateInterpolator( |
| (float) mResources.getInteger(R.integer.drop_deceleration_exponent)); |
| mOnTable = new LinkedList<View>(); |
| mPhotoSource = new PhotoSourcePlexor(getContext(), |
| getContext().getSharedPreferences(PhotoTableDreamSettings.PREFS_NAME, 0)); |
| mLauncher = new Launcher(this); |
| mStarted = false; |
| } |
| |
| |
| public void setDream(DreamService dream) { |
| mDream = dream; |
| } |
| |
| public boolean hasSelection() { |
| return mSelected != null; |
| } |
| |
| public View getSelected() { |
| return mSelected; |
| } |
| |
| public void clearSelection() { |
| mSelected = null; |
| } |
| |
| public void setSelection(View selected) { |
| assert(selected != null); |
| if (mSelected != null) { |
| dropOnTable(mSelected); |
| } |
| mSelected = selected; |
| mSelectedTime = System.currentTimeMillis(); |
| bringChildToFront(selected); |
| pickUp(selected); |
| } |
| |
| 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; |
| } |
| |
| private static PointF randMultiDrop(int n, float i, float j, int width, int height) { |
| log("randMultiDrop (" + n + "," + i + ", " + j + ", " + width + ", " + height + ")"); |
| final float[] cx = {0.3f, 0.3f, 0.5f, 0.7f, 0.7f}; |
| final float[] cy = {0.3f, 0.7f, 0.5f, 0.3f, 0.7f}; |
| n = Math.abs(n); |
| float x = cx[n % cx.length]; |
| float y = cy[n % cx.length]; |
| PointF p = new PointF(); |
| p.x = x * width + 0.05f * width * i; |
| p.y = y * height + 0.05f * 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 { |
| if (mTapToExit && mDream != null) { |
| 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; |
| |
| mLongSide = (int) (mImageRatio * Math.max(mWidth, mHeight)); |
| mShortSide = (int) (mImageRatio * Math.min(mWidth, mHeight)); |
| |
| boolean isLandscape = mWidth > mHeight; |
| if (mIsLandscape != isLandscape) { |
| for (View photo: mOnTable) { |
| if (photo == getSelected()) { |
| pickUp(photo); |
| } else { |
| dropOnTable(photo); |
| } |
| } |
| mIsLandscape = isLandscape; |
| } |
| start(); |
| } |
| |
| @Override |
| public boolean isOpaque() { |
| return true; |
| } |
| |
| private class PhotoLaunchTask extends AsyncTask<Void, Void, View> { |
| private final BitmapFactory.Options mOptions; |
| |
| public PhotoLaunchTask () { |
| mOptions = new BitmapFactory.Options(); |
| mOptions.inTempStorage = new byte[32768]; |
| } |
| |
| @Override |
| public View doInBackground(Void... unused) { |
| log("load a new photo"); |
| final PhotoTable table = PhotoTable.this; |
| |
| LayoutInflater inflater = (LayoutInflater) table.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 = table.mPhotoSource.next(mOptions, |
| table.mLongSide, table.mShortSide); |
| int photoWidth = mOptions.outWidth; |
| int photoHeight = mOptions.outHeight; |
| if (decodedPhoto == null || mOptions.outWidth <= 0 || mOptions.outHeight <= 0) { |
| photo = null; |
| } else { |
| decodedPhoto.setHasMipMap(true); |
| layers[0] = new BitmapDrawable(table.mResources, decodedPhoto); |
| layers[1] = table.mResources.getDrawable(R.drawable.frame); |
| LayerDrawable layerList = new LayerDrawable(layers); |
| layerList.setLayerInset(0, table.mInset, table.mInset, |
| table.mInset, table.mInset); |
| image.setImageDrawable(layerList); |
| |
| photo.setTag(R.id.photo_width, new Integer(photoWidth)); |
| photo.setTag(R.id.photo_height, new Integer(photoHeight)); |
| |
| photo.setOnTouchListener(new PhotoTouchListener(table.getContext(), |
| table)); |
| } |
| |
| return photo; |
| } |
| |
| @Override |
| public void onPostExecute(View photo) { |
| if (photo != null) { |
| final PhotoTable table = PhotoTable.this; |
| |
| table.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT, |
| LayoutParams.WRAP_CONTENT)); |
| if (table.hasSelection()) { |
| table.bringChildToFront(table.getSelected()); |
| } |
| int width = ((Integer) photo.getTag(R.id.photo_width)).intValue(); |
| int height = ((Integer) photo.getTag(R.id.photo_height)).intValue(); |
| |
| log("drop it"); |
| table.throwOnTable(photo); |
| |
| if (mOnTable.size() > mTableCapacity) { |
| int targetSize = Math.max(0, mOnTable.size() - mRedealCount); |
| while (mOnTable.size() > targetSize) { |
| fadeAway(mOnTable.poll(), false); |
| } |
| } |
| |
| if(table.mOnTable.size() < table.mTableCapacity) { |
| table.scheduleNext(table.mFastDropPeriod); |
| } |
| } |
| } |
| }; |
| |
| public void launch() { |
| log("launching"); |
| setSystemUiVisibility(View.STATUS_BAR_HIDDEN); |
| if (hasSelection() && |
| (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) { |
| dropOnTable(getSelected()); |
| clearSelection(); |
| } else { |
| log("inflate it"); |
| if (mPhotoLaunchTask == null || |
| mPhotoLaunchTask.getStatus() == AsyncTask.Status.FINISHED) { |
| mPhotoLaunchTask = new PhotoLaunchTask(); |
| mPhotoLaunchTask.execute(); |
| } |
| } |
| } |
| public void fadeAway(final View photo, final boolean replace) { |
| // fade out of view |
| mOnTable.remove(photo); |
| photo.animate().cancel(); |
| photo.animate() |
| .withLayer() |
| .alpha(0f) |
| .setDuration(1000) |
| .withEndAction(new Runnable() { |
| @Override |
| public void run() { |
| removeView(photo); |
| recycle(photo); |
| if (replace) { |
| scheduleNext(mNowDropDelay); |
| } |
| } |
| }); |
| } |
| |
| public void moveToBackOfQueue(View photo) { |
| // make this photo the last to be removed. |
| bringChildToFront(photo); |
| invalidate(); |
| mOnTable.remove(photo); |
| mOnTable.offer(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(mThrowRotation); |
| photo.setX(-mLongSide); |
| photo.setY(-mLongSide); |
| |
| dropOnTable(photo, mThrowInterpolator); |
| } |
| |
| public void dropOnTable(final View photo) { |
| dropOnTable(photo, mDropInterpolator); |
| } |
| |
| public void dropOnTable(final View photo, final Interpolator interpolator) { |
| float angle = randfrange(-mImageRotationLimit, mImageRotationLimit); |
| PointF p = randMultiDrop(sRNG.nextInt(), |
| (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 -= mLongSide / 2f; |
| y -= mShortSide / 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 / mThrowSpeed); |
| duration = Math.max(duration, 1000); |
| |
| log("animate it"); |
| // toss onto table |
| photo.animate() |
| .scaleX(mTableRatio / mImageRatio) |
| .scaleY(mTableRatio / mImageRatio) |
| .rotation(angle) |
| .x(x) |
| .y(y) |
| .setDuration(duration) |
| .setInterpolator(interpolator); |
| } |
| |
| /** wrap all orientations to the interval [-180, 180). */ |
| private float wrapAngle(float angle) { |
| float result = angle + 180; |
| result = ((result % 360) + 360) % 360; // catch negative numbers |
| result -= 180; |
| return result; |
| } |
| |
| 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 / 600f); |
| duration = Math.max(duration, 500); |
| |
| photo.setRotation(wrapAngle(photo.getRotation())); |
| |
| log("animate it"); |
| // toss onto table |
| photo.animate() |
| .rotation(0f) |
| .scaleX(scale) |
| .scaleY(scale) |
| .x(x) |
| .y(y) |
| .setDuration(duration) |
| .setInterpolator(new DecelerateInterpolator(2f)) |
| .withEndAction(new Runnable() { |
| @Override |
| public void run() { |
| log("endtimes: " + photo.getX()); |
| } |
| }); |
| } |
| |
| 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 start() { |
| if (!mStarted) { |
| log("kick it"); |
| mStarted = true; |
| scheduleNext(mDropPeriod); |
| launch(); |
| } |
| } |
| |
| public void scheduleNext(int delay) { |
| removeCallbacks(mLauncher); |
| postDelayed(mLauncher, delay); |
| } |
| |
| private static void log(String message) { |
| if (DEBUG) { |
| Log.i(TAG, message); |
| } |
| } |
| } |