diff options
author | Martijn Coenen <maco@google.com> | 2011-08-01 17:39:18 -0700 |
---|---|---|
committer | Martijn Coenen <maco@google.com> | 2011-08-02 12:55:59 -0700 |
commit | 84b7f15e1a51c3ddd801aaf35bd5c7033e976d61 (patch) | |
tree | 65b75ce1fcb6e956ab2a8fe81033af6f0f8fd90c /src/com/android/nfc | |
parent | 5ce39aec169a275e814286372256277819593829 (diff) | |
download | packages_apps_nfc-84b7f15e1a51c3ddd801aaf35bd5c7033e976d61.zip packages_apps_nfc-84b7f15e1a51c3ddd801aaf35bd5c7033e976d61.tar.gz packages_apps_nfc-84b7f15e1a51c3ddd801aaf35bd5c7033e976d61.tar.bz2 |
Animations for 0-click send success / failure.
This implements the following behavior:
- The first part of the animation (the screen scaling down)
is always played and finished.
- Depending on the result, the following animation will either
pop the screen back to front (failure), or "clone and swap"
(success). Both animations will be fully played, except
if there is a new start() request, in which case they are ended
immediately.
The main reason to fully play both animations is that the LLCP
transfer typically completes very quickly, and with me profile
removed, none of the animations would ever be visible. I think
the animations are short enough not to be annoying, but we can
tweak depending on user feedback.
Change-Id: Iecd883fec43fad3eeb35f03b6076562b36a10fef
Diffstat (limited to 'src/com/android/nfc')
-rwxr-xr-x | src/com/android/nfc/NdefP2pManager.java | 4 | ||||
-rw-r--r-- | src/com/android/nfc/ScreenshotWindowAnimator.java | 320 |
2 files changed, 278 insertions, 46 deletions
diff --git a/src/com/android/nfc/NdefP2pManager.java b/src/com/android/nfc/NdefP2pManager.java index 20f2ec8..affb9a3 100755 --- a/src/com/android/nfc/NdefP2pManager.java +++ b/src/com/android/nfc/NdefP2pManager.java @@ -253,7 +253,7 @@ public class NdefP2pManager { @Override public void onCancelled() { if (mMessage != null) { - mScreenshot.stop(); + mScreenshot.complete(mSuccess); // Call error here since a previous call to onP2pEnd() will have played the success // sound and this will be ignored. If no one has called that and the LLCP link // is broken we want to play the error sound. @@ -266,7 +266,7 @@ public class NdefP2pManager { public void onPostExecute(Void result) { // Make sure to stop the screenshot animation if (mMessage != null) { - mScreenshot.stop(); + mScreenshot.complete(mSuccess); } // Play the end sound if successful diff --git a/src/com/android/nfc/ScreenshotWindowAnimator.java b/src/com/android/nfc/ScreenshotWindowAnimator.java index dd81b34..5144bb9 100644 --- a/src/com/android/nfc/ScreenshotWindowAnimator.java +++ b/src/com/android/nfc/ScreenshotWindowAnimator.java @@ -20,6 +20,7 @@ import com.android.nfc3.R; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; @@ -38,23 +39,29 @@ import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; +import java.util.ArrayList; +import java.util.List; + /** * TODO: * - Performance when over gl surfaces? Ie. Gallery * - what do we say in the Toast? Which icon do we get if the user uses another * type of gallery? */ -public class ScreenshotWindowAnimator implements AnimatorUpdateListener, Handler.Callback { +public class ScreenshotWindowAnimator implements Handler.Callback, AnimatorUpdateListener { private static final String TAG = "ScreenshotWindowAnimator"; - private static final int ANIMATION_DURATION_MS = 7500; - private static final float SCREENSHOT_SCALE = 0.5f; + private static final float INITIAL_SCREENSHOT_SCALE = 0.7f; + private static final float FINAL_SCREENSHOT_SCALE = 0.3f; private static final int MSG_START_ANIMATION = 1; - private static final int MSG_STOP_ANIMATION = 2; + private static final int MSG_START_SUCCESS_ANIMATION = 2; + private static final int MSG_START_FAIL_ANIMATION = 3; + private static final int MSG_STOP_ANIMATIONS = 4; Context mContext; LayoutInflater mLayoutInflater; @@ -66,13 +73,62 @@ public class ScreenshotWindowAnimator implements AnimatorUpdateListener, Handler Bitmap mScreenBitmap; View mScreenshotLayout; ImageView mScreenshotView; -// Animator mScreenshotAnimator; - Listener mListener; - ValueAnimator mAnimator; + ImageView mClonedView; + int mScreenshotWidth; + + StartAnimationListener mStartListener; + EndAnimationListener mEndListener; + + AnimatorSet mSuccessAnimatorSet; + + ValueAnimator mStartAnimator; + ValueAnimator mScaleDownAnimator; + ValueAnimator mScaleUpAnimator; + ValueAnimator mFailureAnimator; + + // Down animation + DecelerateInterpolator mScaleDownInterpolator; + DecelerateInterpolator mAlphaDownInterpolator; + DecelerateInterpolator mOffsetInterpolator; + + // Up animation + AccelerateInterpolator mScaleUpInterpolator; + AccelerateInterpolator mAlphaUpInterpolator; + AccelerateInterpolator mCloneScaleDownInterpolator; + + + // These are all read/written on the UI thread, so no + // need to synchronize these. + // TODO state var could clean this up a lot. boolean mAttached = false; + boolean mWaitingForResult = true; + boolean mSuccess = false; + boolean mStartAnimDone = false; + boolean mEndRequested = false; + Handler mHandler; - class Listener extends AnimatorListenerAdapter { + class StartAnimationListener extends AnimatorListenerAdapter { + @Override + // Note that this will be called on the UI thread! + public void onAnimationEnd(Animator animation) { + if (mEndRequested) { + // Ended on request, don't start follow-up anim + // and get rid of the view + if (mAttached) { + mWindowManager.removeView(mScreenshotLayout); + mAttached = false; + } + } else { + mStartAnimDone = true; + if (!mWaitingForResult) { // Result already in + endAnimation(mSuccess); + } // else, wait for it + } + } + } + + class EndAnimationListener extends AnimatorListenerAdapter { @Override public void onAnimationEnd(Animator animation) { if (mAttached) { @@ -82,6 +138,65 @@ public class ScreenshotWindowAnimator implements AnimatorUpdateListener, Handler } } + void endAnimation(boolean success) { + mHandler.sendEmptyMessage(success ? MSG_START_SUCCESS_ANIMATION : + MSG_START_FAIL_ANIMATION); + } + + void createAnimators() { + mStartListener = new StartAnimationListener(); + mEndListener = new EndAnimationListener(); + + // Create the starting scale down animation + ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); + anim.setInterpolator(null); // Linear time interpolation + anim.setDuration(500); // 500 ms to scale down + anim.addUpdateListener(this); + anim.addListener(mStartListener); + mStartAnimator = anim; + + // Create the cloned scale down animation + // (First part of animation when successfully sending) + anim = ValueAnimator.ofFloat(0f, 1f); + anim.setInterpolator(null); + anim.setDuration(500); + anim.addUpdateListener(this); + mScaleDownAnimator = anim; + + // Create the scale up animation + // (Second part of animation when successfully sending) + anim = ValueAnimator.ofFloat(0f, 1f); + anim.setInterpolator(null); + anim.setDuration(500); + anim.addUpdateListener(this); + mScaleUpAnimator = anim; + + // Combine the two in a set + mSuccessAnimatorSet = new AnimatorSet(); + List<Animator> animList = new ArrayList<Animator>(); + animList.add(mScaleDownAnimator); + animList.add(mScaleUpAnimator); + mSuccessAnimatorSet.playSequentially(animList); + mSuccessAnimatorSet.addListener(mEndListener); + + // Create the failure animator + anim = ValueAnimator.ofFloat(0f, 1f); + anim.setInterpolator(null); + anim.setDuration(500); + anim.addUpdateListener(this); + anim.addListener(mEndListener); + mFailureAnimator = anim; + + mScaleDownInterpolator = new DecelerateInterpolator(1.5f); + mAlphaDownInterpolator = new DecelerateInterpolator(1f); + mOffsetInterpolator = new DecelerateInterpolator(1.5f); + + mScaleUpInterpolator = new AccelerateInterpolator(1.5f); + mAlphaUpInterpolator = new AccelerateInterpolator(1.5f); + mCloneScaleDownInterpolator = new AccelerateInterpolator(1.0f); + + } + /** * @param context everything needs a context :( */ @@ -91,35 +206,28 @@ public class ScreenshotWindowAnimator implements AnimatorUpdateListener, Handler mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mListener = new Listener(); + createAnimators(); - // Create the animation - ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); - anim.setInterpolator(new DecelerateInterpolator()); - anim.setDuration(ANIMATION_DURATION_MS); - anim.addUpdateListener(this); - anim.addListener(mListener); - mAnimator = anim; - // Inflate the screenshot layout mDisplayMetrics = new DisplayMetrics(); mDisplayMatrix = new Matrix(); mScreenshotLayout = mLayoutInflater.inflate(R.layout.screenshot, null); + mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.screenshot); + mClonedView = (ImageView) mScreenshotLayout.findViewById(R.id.clone); mScreenshotLayout.setFocusable(true); -/* this doesn't work on the TYPE_SYSTEM_OVERLAY layer - mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - // Intercept and ignore all touch events - return true; - } - }); -*/ + /* + * this doesn't work on the TYPE_SYSTEM_OVERLAY layer + * mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() { + * + * @Override public boolean onTouch(View v, MotionEvent event) { // + * Intercept and ignore all touch events return true; } }); + */ // Setup the window that we are going to use - mWindowLayoutParams = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, + mWindowLayoutParams = + new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED @@ -154,7 +262,8 @@ public class ScreenshotWindowAnimator implements AnimatorUpdateListener, Handler * Must be called from the UI thread. */ public void start() { - // We need to orient the screenshot correctly (and the Surface api seems to take screenshots + // We need to orient the screenshot correctly (and the Surface api seems to + // take screenshots // only in the natural orientation of the device :!) mDisplay.getRealMetrics(mDisplayMetrics); float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels}; @@ -185,56 +294,179 @@ public class ScreenshotWindowAnimator implements AnimatorUpdateListener, Handler c.rotate(360f - degrees); c.translate(-dims[0] / 2, -dims[1] / 2); c.drawBitmap(mScreenBitmap, 0, 0, null); + mScreenBitmap = ss; } + // The clone is hidden at the beginning + mClonedView.setImageBitmap(mScreenBitmap); + mClonedView.setVisibility(View.GONE); + // Start the post-screenshot animation + mScreenshotView.setImageBitmap(mScreenBitmap); + mScreenshotLayout.requestFocus(); - // Setup the animation with the screenshot just taken - if (mAnimator != null) { - mAnimator.end(); - } + mScreenshotWidth = mScreenBitmap.getWidth(); + + // Make sure any existing animations are ended + endAnimations(); + + // At this point no anims are running, no need to sync these + mSuccess = false; + mWaitingForResult = true; + mStartAnimDone = false; + mEndRequested = false; // Add the view for the animation mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); + mAttached = true; mHandler.sendEmptyMessage(MSG_START_ANIMATION); } + private void endAnimations() { + mEndRequested = true; + + if (mStartAnimator != null) { + mStartAnimator.end(); + } + if (mSuccessAnimatorSet != null) { + mSuccessAnimatorSet.end(); + } + if (mFailureAnimator != null) { + mFailureAnimator.end(); + } + } + + /** + * Finalizes the running animation with either a success or a failure + * animation. + * Must be called from the UI thread. + */ + public void complete(boolean result) { + mSuccess = result; + mWaitingForResult = false; + if (mStartAnimDone) { + endAnimation(result); + } // else will show result anim when start anim is done + } + /** * Stops the currently playing animation. * * Must be called from the UI thread. */ public void stop() { - mHandler.sendEmptyMessage(MSG_STOP_ANIMATION); + mHandler.sendEmptyMessage(MSG_STOP_ANIMATIONS); } - @Override - public void onAnimationUpdate(ValueAnimator animation) { + private void onStartAnimationUpdate(ValueAnimator animation) { + // Just scale the screenshot down + float t = ((Float) animation.getAnimatedValue()).floatValue(); + float scale = mScaleDownInterpolator.getInterpolation(t); + float scaleT = INITIAL_SCREENSHOT_SCALE + (1f - scale) * + (1 - INITIAL_SCREENSHOT_SCALE); + + mScreenshotView.setScaleX(scaleT); + mScreenshotView.setScaleY(scaleT); + } + + private void onSuccessCloneAnimationUpdate(ValueAnimator animation) { + // Clone the screenshot, split the two and scale them down further + if (mClonedView.getVisibility() != View.VISIBLE) { + mClonedView.setVisibility(View.VISIBLE); + } + + float t = ((Float) animation.getAnimatedValue()).floatValue(); + float scale = mScaleDownInterpolator.getInterpolation(t); + float scaleT = INITIAL_SCREENSHOT_SCALE - (scale * + (INITIAL_SCREENSHOT_SCALE - FINAL_SCREENSHOT_SCALE)); + + float cloneAlpha = mAlphaDownInterpolator.getInterpolation(t) * 0.5f; + + float offset = mOffsetInterpolator.getInterpolation(t); + mScreenshotView.setScaleX(scaleT); + mScreenshotView.setScaleY(scaleT); + + mClonedView.setScaleX(scaleT); + mClonedView.setScaleY(scaleT); + mClonedView.setAlpha(cloneAlpha); + + mScreenshotView.setX((int) (offset * mScreenshotWidth / 4)); + mClonedView.setX((int) (offset * -mScreenshotWidth / 4)); + } + + private void onSuccessUpUpdate(ValueAnimator animation) { + // Scale the screenshot all the way back to the front, + // scale the clone down to zero. + float t = ((Float) animation.getAnimatedValue()).floatValue(); + float scale = mScaleDownInterpolator.getInterpolation(t); + float scaleT = FINAL_SCREENSHOT_SCALE + + (scale * (1.0f - FINAL_SCREENSHOT_SCALE)); + float scaleClone = FINAL_SCREENSHOT_SCALE - + (scale * FINAL_SCREENSHOT_SCALE); + + float cloneAlpha = 0.5f + 0.5f * mAlphaDownInterpolator.getInterpolation(t); + + mScreenshotView.setScaleX(scaleT); + mScreenshotView.setScaleY(scaleT); + + mClonedView.setScaleX(scaleClone); + mClonedView.setScaleY(scaleClone); + mClonedView.setAlpha(cloneAlpha); + + float offset = 1 - mOffsetInterpolator.getInterpolation(t); + mScreenshotView.setX((int) (offset * mScreenshotWidth / 4)); + } + + private void onFailureUpdate(ValueAnimator animation) { + // Scale back from initial scale to normal scale + // TODO add some shaking float t = ((Float) animation.getAnimatedValue()).floatValue(); - float scaleT = SCREENSHOT_SCALE + (1f - t) * SCREENSHOT_SCALE; + float scale = mScaleDownInterpolator.getInterpolation(t); + float scaleT = INITIAL_SCREENSHOT_SCALE + (scale * + (1.0f - INITIAL_SCREENSHOT_SCALE)); + mScreenshotView.setScaleX(scaleT); mScreenshotView.setScaleY(scaleT); + + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (animation == mStartAnimator) { + onStartAnimationUpdate(animation); + } else if (animation == mScaleDownAnimator) { + onSuccessCloneAnimationUpdate(animation); + } else if (animation == mScaleUpAnimator) { + onSuccessUpUpdate(animation); + } else if (animation == mFailureAnimator) { + onFailureUpdate(animation); + } } @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_START_ANIMATION: { - mAnimator.start(); + mStartAnimator.start(); break; } - - case MSG_STOP_ANIMATION: { - if (mAnimator != null) { - mAnimator.cancel(); - } + case MSG_START_SUCCESS_ANIMATION: { + mSuccessAnimatorSet.start(); + break; + } + case MSG_START_FAIL_ANIMATION: { + mFailureAnimator.start(); + break; + } + case MSG_STOP_ANIMATIONS: { + endAnimations(); break; } } return true; } -}
\ No newline at end of file +} |