GlobalScreenshot.java revision 9c99aee5b9ea0b929611548b48780fbffa4daeaf
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.systemui.screenshot; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ValueAnimator; 23import android.animation.ValueAnimator.AnimatorUpdateListener; 24import android.app.Notification; 25import android.app.NotificationManager; 26import android.app.PendingIntent; 27import android.content.ContentResolver; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.Intent; 31import android.content.res.Resources; 32import android.graphics.Bitmap; 33import android.graphics.Canvas; 34import android.graphics.Matrix; 35import android.graphics.PixelFormat; 36import android.graphics.PointF; 37import android.net.Uri; 38import android.os.AsyncTask; 39import android.os.Environment; 40import android.os.Process; 41import android.os.ServiceManager; 42import android.provider.MediaStore; 43import android.util.DisplayMetrics; 44import android.view.Display; 45import android.view.IWindowManager; 46import android.view.LayoutInflater; 47import android.view.MotionEvent; 48import android.view.Surface; 49import android.view.View; 50import android.view.ViewGroup; 51import android.view.WindowManager; 52import android.view.animation.Interpolator; 53import android.widget.FrameLayout; 54import android.widget.ImageView; 55 56import com.android.systemui.R; 57 58import java.io.File; 59import java.io.OutputStream; 60import java.text.SimpleDateFormat; 61import java.util.Date; 62 63/** 64 * POD used in the AsyncTask which saves an image in the background. 65 */ 66class SaveImageInBackgroundData { 67 Context context; 68 Bitmap image; 69 Uri imageUri; 70 Runnable finisher; 71 int iconSize; 72 int result; 73} 74 75/** 76 * An AsyncTask that saves an image to the media store in the background. 77 */ 78class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Void, 79 SaveImageInBackgroundData> { 80 private static final String TAG = "SaveImageInBackgroundTask"; 81 private static final String SCREENSHOTS_DIR_NAME = "Screenshots"; 82 private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png"; 83 private static final String SCREENSHOT_FILE_PATH_TEMPLATE = "%s/%s/%s"; 84 85 private int mNotificationId; 86 private NotificationManager mNotificationManager; 87 private Notification.Builder mNotificationBuilder; 88 private Intent mLaunchIntent; 89 private String mImageDir; 90 private String mImageFileName; 91 private String mImageFilePath; 92 private String mImageDate; 93 private long mImageTime; 94 95 // WORKAROUND: We want the same notification across screenshots that we update so that we don't 96 // spam a user's notification drawer. However, we only show the ticker for the saving state 97 // and if the ticker text is the same as the previous notification, then it will not show. So 98 // for now, we just add and remove a space from the ticker text to trigger the animation when 99 // necessary. 100 private static boolean mTickerAddSpace; 101 102 SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data, 103 NotificationManager nManager, int nId) { 104 Resources r = context.getResources(); 105 106 // Prepare all the output metadata 107 mImageTime = System.currentTimeMillis(); 108 mImageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(mImageTime)); 109 mImageDir = Environment.getExternalStoragePublicDirectory( 110 Environment.DIRECTORY_PICTURES).getAbsolutePath(); 111 mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, mImageDate); 112 mImageFilePath = String.format(SCREENSHOT_FILE_PATH_TEMPLATE, mImageDir, 113 SCREENSHOTS_DIR_NAME, mImageFileName); 114 115 // Create the large notification icon 116 int imageWidth = data.image.getWidth(); 117 int imageHeight = data.image.getHeight(); 118 int iconWidth = data.iconSize; 119 int iconHeight = data.iconSize; 120 if (imageWidth > imageHeight) { 121 iconWidth = (int) (((float) iconHeight / imageHeight) * imageWidth); 122 } else { 123 iconHeight = (int) (((float) iconWidth / imageWidth) * imageHeight); 124 } 125 Bitmap rawIcon = Bitmap.createScaledBitmap(data.image, iconWidth, iconHeight, true); 126 Bitmap croppedIcon = Bitmap.createBitmap(rawIcon, (iconWidth - data.iconSize) / 2, 127 (iconHeight - data.iconSize) / 2, data.iconSize, data.iconSize); 128 129 // Show the intermediate notification 130 mTickerAddSpace = !mTickerAddSpace; 131 mNotificationId = nId; 132 mNotificationManager = nManager; 133 mNotificationBuilder = new Notification.Builder(context) 134 .setTicker(r.getString(R.string.screenshot_saving_ticker) 135 + (mTickerAddSpace ? " " : "")) 136 .setContentTitle(r.getString(R.string.screenshot_saving_title)) 137 .setContentText(r.getString(R.string.screenshot_saving_text)) 138 .setSmallIcon(R.drawable.stat_notify_image) 139 .setWhen(System.currentTimeMillis()); 140 Notification n = mNotificationBuilder.getNotification(); 141 n.flags |= Notification.FLAG_NO_CLEAR; 142 mNotificationManager.notify(nId, n); 143 144 // On the tablet, the large icon makes the notification appear as if it is clickable (and 145 // on small devices, the large icon is not shown) so defer showing the large icon until 146 // we compose the final post-save notification below. 147 mNotificationBuilder.setLargeIcon(croppedIcon); 148 } 149 150 @Override 151 protected SaveImageInBackgroundData doInBackground(SaveImageInBackgroundData... params) { 152 if (params.length != 1) return null; 153 154 // By default, AsyncTask sets the worker thread to have background thread priority, so bump 155 // it back up so that we save a little quicker. 156 Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); 157 158 Context context = params[0].context; 159 Bitmap image = params[0].image; 160 161 try { 162 // Save the screenshot to the MediaStore 163 ContentValues values = new ContentValues(); 164 ContentResolver resolver = context.getContentResolver(); 165 values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath); 166 values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName); 167 values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName); 168 values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime); 169 values.put(MediaStore.Images.ImageColumns.DATE_ADDED, mImageTime); 170 values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, mImageTime); 171 values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png"); 172 Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); 173 174 OutputStream out = resolver.openOutputStream(uri); 175 image.compress(Bitmap.CompressFormat.PNG, 100, out); 176 out.flush(); 177 out.close(); 178 179 // update file size in the database 180 values.clear(); 181 values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length()); 182 resolver.update(uri, values, null, null); 183 184 params[0].imageUri = uri; 185 params[0].result = 0; 186 } catch (Exception e) { 187 // IOException/UnsupportedOperationException may be thrown if external storage is not 188 // mounted 189 params[0].result = 1; 190 } 191 192 return params[0]; 193 }; 194 195 @Override 196 protected void onPostExecute(SaveImageInBackgroundData params) { 197 if (params.result > 0) { 198 // Show a message that we've failed to save the image to disk 199 GlobalScreenshot.notifyScreenshotError(params.context, mNotificationManager); 200 } else { 201 // Show the final notification to indicate screenshot saved 202 Resources r = params.context.getResources(); 203 204 // Create the intent to show the screenshot in gallery 205 mLaunchIntent = new Intent(Intent.ACTION_VIEW); 206 mLaunchIntent.setDataAndType(params.imageUri, "image/png"); 207 mLaunchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 208 209 mNotificationBuilder 210 .setContentTitle(r.getString(R.string.screenshot_saved_title)) 211 .setContentText(r.getString(R.string.screenshot_saved_text)) 212 .setContentIntent(PendingIntent.getActivity(params.context, 0, mLaunchIntent, 0)) 213 .setWhen(System.currentTimeMillis()) 214 .setAutoCancel(true); 215 216 Notification n = mNotificationBuilder.getNotification(); 217 n.flags &= ~Notification.FLAG_NO_CLEAR; 218 mNotificationManager.notify(mNotificationId, n); 219 } 220 params.finisher.run(); 221 }; 222} 223 224/** 225 * TODO: 226 * - Performance when over gl surfaces? Ie. Gallery 227 * - what do we say in the Toast? Which icon do we get if the user uses another 228 * type of gallery? 229 */ 230class GlobalScreenshot { 231 private static final String TAG = "GlobalScreenshot"; 232 private static final int SCREENSHOT_NOTIFICATION_ID = 789; 233 private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130; 234 private static final int SCREENSHOT_DROP_IN_DURATION = 430; 235 private static final int SCREENSHOT_DROP_OUT_DELAY = 500; 236 private static final int SCREENSHOT_DROP_OUT_DURATION = 430; 237 private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370; 238 private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320; 239 private static final float BACKGROUND_ALPHA = 0.5f; 240 private static final float SCREENSHOT_SCALE = 1f; 241 private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f; 242 private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f; 243 private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f; 244 private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f; 245 246 private Context mContext; 247 private LayoutInflater mLayoutInflater; 248 private IWindowManager mIWindowManager; 249 private WindowManager mWindowManager; 250 private WindowManager.LayoutParams mWindowLayoutParams; 251 private NotificationManager mNotificationManager; 252 private Display mDisplay; 253 private DisplayMetrics mDisplayMetrics; 254 private Matrix mDisplayMatrix; 255 256 private Bitmap mScreenBitmap; 257 private View mScreenshotLayout; 258 private ImageView mBackgroundView; 259 private FrameLayout mScreenshotContainerView; 260 private ImageView mScreenshotView; 261 private ImageView mScreenshotFlash; 262 263 private AnimatorSet mScreenshotAnimation; 264 265 private int mNotificationIconSize; 266 private float mBgPadding; 267 private float mBgPaddingScale; 268 269 270 /** 271 * @param context everything needs a context :( 272 */ 273 public GlobalScreenshot(Context context) { 274 Resources r = context.getResources(); 275 mContext = context; 276 mLayoutInflater = (LayoutInflater) 277 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 278 279 // Inflate the screenshot layout 280 mDisplayMatrix = new Matrix(); 281 mScreenshotLayout = mLayoutInflater.inflate(R.layout.global_screenshot, null); 282 mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background); 283 mScreenshotContainerView = (FrameLayout) mScreenshotLayout.findViewById(R.id.global_screenshot_container); 284 mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot); 285 mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash); 286 mScreenshotLayout.setFocusable(true); 287 mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() { 288 @Override 289 public boolean onTouch(View v, MotionEvent event) { 290 // Intercept and ignore all touch events 291 return true; 292 } 293 }); 294 295 // Setup the window that we are going to use 296 mIWindowManager = IWindowManager.Stub.asInterface( 297 ServiceManager.getService(Context.WINDOW_SERVICE)); 298 mWindowLayoutParams = new WindowManager.LayoutParams( 299 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, 300 WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, 301 WindowManager.LayoutParams.FLAG_FULLSCREEN 302 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED 303 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 304 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, 305 PixelFormat.TRANSLUCENT); 306 mWindowLayoutParams.setTitle("ScreenshotAnimation"); 307 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 308 mNotificationManager = 309 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 310 mDisplay = mWindowManager.getDefaultDisplay(); 311 mDisplayMetrics = new DisplayMetrics(); 312 mDisplay.getRealMetrics(mDisplayMetrics); 313 314 // Get the various target sizes 315 mNotificationIconSize = 316 r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 317 318 // Scale has to account for both sides of the bg 319 mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding); 320 mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels; 321 } 322 323 /** 324 * Creates a new worker thread and saves the screenshot to the media store. 325 */ 326 private void saveScreenshotInWorkerThread(Runnable finisher) { 327 SaveImageInBackgroundData data = new SaveImageInBackgroundData(); 328 data.context = mContext; 329 data.image = mScreenBitmap; 330 data.iconSize = mNotificationIconSize; 331 data.finisher = finisher; 332 new SaveImageInBackgroundTask(mContext, data, mNotificationManager, 333 SCREENSHOT_NOTIFICATION_ID).execute(data); 334 } 335 336 /** 337 * @return the current display rotation in degrees 338 */ 339 private float getDegreesForRotation(int value) { 340 switch (value) { 341 case Surface.ROTATION_90: 342 return 360f - 90f; 343 case Surface.ROTATION_180: 344 return 360f - 180f; 345 case Surface.ROTATION_270: 346 return 360f - 270f; 347 } 348 return 0f; 349 } 350 351 /** 352 * Takes a screenshot of the current display and shows an animation. 353 */ 354 void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { 355 // We need to orient the screenshot correctly (and the Surface api seems to take screenshots 356 // only in the natural orientation of the device :!) 357 mDisplay.getRealMetrics(mDisplayMetrics); 358 float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels}; 359 float degrees = getDegreesForRotation(mDisplay.getRotation()); 360 boolean requiresRotation = (degrees > 0); 361 if (requiresRotation) { 362 // Get the dimensions of the device in its native orientation 363 mDisplayMatrix.reset(); 364 mDisplayMatrix.preRotate(-degrees); 365 mDisplayMatrix.mapPoints(dims); 366 dims[0] = Math.abs(dims[0]); 367 dims[1] = Math.abs(dims[1]); 368 } 369 mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]); 370 if (requiresRotation) { 371 // Rotate the screenshot to the current orientation 372 Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels, 373 mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888); 374 Canvas c = new Canvas(ss); 375 c.translate(ss.getWidth() / 2, ss.getHeight() / 2); 376 c.rotate(degrees); 377 c.translate(-dims[0] / 2, -dims[1] / 2); 378 c.drawBitmap(mScreenBitmap, 0, 0, null); 379 c.setBitmap(null); 380 mScreenBitmap = ss; 381 } 382 383 // If we couldn't take the screenshot, notify the user 384 if (mScreenBitmap == null) { 385 notifyScreenshotError(mContext, mNotificationManager); 386 finisher.run(); 387 return; 388 } 389 390 // Optimizations 391 mScreenBitmap.setHasAlpha(false); 392 mScreenBitmap.prepareToDraw(); 393 394 // Start the post-screenshot animation 395 startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels, 396 statusBarVisible, navBarVisible); 397 } 398 399 400 /** 401 * Starts the animation after taking the screenshot 402 */ 403 private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, 404 boolean navBarVisible) { 405 // Add the view for the animation 406 mScreenshotView.setImageBitmap(mScreenBitmap); 407 mScreenshotLayout.requestFocus(); 408 409 // Setup the animation with the screenshot just taken 410 if (mScreenshotAnimation != null) { 411 mScreenshotAnimation.end(); 412 } 413 414 mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); 415 ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation(); 416 ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h, 417 statusBarVisible, navBarVisible); 418 mScreenshotAnimation = new AnimatorSet(); 419 mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim); 420 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 421 @Override 422 public void onAnimationEnd(Animator animation) { 423 // Save the screenshot once we have a bit of time now 424 saveScreenshotInWorkerThread(finisher); 425 mWindowManager.removeView(mScreenshotLayout); 426 } 427 }); 428 mScreenshotLayout.post(new Runnable() { 429 @Override 430 public void run() { 431 mScreenshotContainerView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 432 mScreenshotContainerView.buildLayer(); 433 mScreenshotAnimation.start(); 434 } 435 }); 436 } 437 private ValueAnimator createScreenshotDropInAnimation() { 438 final float flashPeakDurationPct = ((float) (SCREENSHOT_FLASH_TO_PEAK_DURATION) 439 / SCREENSHOT_DROP_IN_DURATION); 440 final float flashDurationPct = 2f * flashPeakDurationPct; 441 final Interpolator flashAlphaInterpolator = new Interpolator() { 442 @Override 443 public float getInterpolation(float x) { 444 // Flash the flash view in and out quickly 445 if (x <= flashDurationPct) { 446 return (float) Math.sin(Math.PI * (x / flashDurationPct)); 447 } 448 return 0; 449 } 450 }; 451 final Interpolator scaleInterpolator = new Interpolator() { 452 @Override 453 public float getInterpolation(float x) { 454 // We start scaling when the flash is at it's peak 455 if (x < flashPeakDurationPct) { 456 return 0; 457 } 458 return (x - flashDurationPct) / (1f - flashDurationPct); 459 } 460 }; 461 ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); 462 anim.setDuration(SCREENSHOT_DROP_IN_DURATION); 463 anim.addListener(new AnimatorListenerAdapter() { 464 @Override 465 public void onAnimationStart(Animator animation) { 466 mBackgroundView.setFastAlpha(0f); 467 mBackgroundView.setVisibility(View.VISIBLE); 468 mBackgroundView.fastInvalidate(); 469 mScreenshotContainerView.setFastAlpha(0f); 470 mScreenshotContainerView.setFastTranslationX(0f); 471 mScreenshotContainerView.setFastTranslationY(0f); 472 mScreenshotContainerView.setFastScaleX(SCREENSHOT_SCALE + mBgPaddingScale); 473 mScreenshotContainerView.setFastScaleY(SCREENSHOT_SCALE + mBgPaddingScale); 474 mScreenshotContainerView.setVisibility(View.VISIBLE); 475 mScreenshotContainerView.fastInvalidate(); 476 mScreenshotFlash.setFastAlpha(0f); 477 mScreenshotFlash.setVisibility(View.VISIBLE); 478 mScreenshotFlash.fastInvalidate(); 479 mScreenshotLayout.invalidate(); 480 } 481 @Override 482 public void onAnimationEnd(android.animation.Animator animation) { 483 mScreenshotFlash.setVisibility(View.GONE); 484 } 485 }); 486 anim.addUpdateListener(new AnimatorUpdateListener() { 487 @Override 488 public void onAnimationUpdate(ValueAnimator animation) { 489 float t = ((Float) animation.getAnimatedValue()).floatValue(); 490 float scaleT = (SCREENSHOT_SCALE + mBgPaddingScale) 491 - (float) scaleInterpolator.getInterpolation(t) 492 * (SCREENSHOT_SCALE - SCREENSHOT_DROP_IN_MIN_SCALE); 493 mBackgroundView.setFastAlpha(scaleInterpolator.getInterpolation(t) * BACKGROUND_ALPHA); 494 mBackgroundView.fastInvalidate(); 495 mScreenshotContainerView.setFastAlpha(t); 496 mScreenshotContainerView.setFastScaleX(scaleT); 497 mScreenshotContainerView.setFastScaleY(scaleT); 498 mScreenshotContainerView.fastInvalidate(); 499 mScreenshotFlash.setFastAlpha(flashAlphaInterpolator.getInterpolation(t)); 500 mScreenshotFlash.fastInvalidate(); 501 mScreenshotLayout.invalidate(); 502 } 503 }); 504 return anim; 505 } 506 private ValueAnimator createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible, 507 boolean navBarVisible) { 508 ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); 509 anim.setStartDelay(SCREENSHOT_DROP_OUT_DELAY); 510 anim.addListener(new AnimatorListenerAdapter() { 511 @Override 512 public void onAnimationEnd(Animator animation) { 513 mBackgroundView.setVisibility(View.GONE); 514 mScreenshotContainerView.setVisibility(View.GONE); 515 mScreenshotContainerView.setLayerType(View.LAYER_TYPE_NONE, null); 516 } 517 }); 518 519 if (!statusBarVisible || !navBarVisible) { 520 // There is no status bar/nav bar, so just fade the screenshot away in place 521 anim.setDuration(SCREENSHOT_FAST_DROP_OUT_DURATION); 522 anim.addUpdateListener(new AnimatorUpdateListener() { 523 @Override 524 public void onAnimationUpdate(ValueAnimator animation) { 525 float t = ((Float) animation.getAnimatedValue()).floatValue(); 526 float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale) 527 - (float) t * (SCREENSHOT_DROP_IN_MIN_SCALE 528 - SCREENSHOT_FAST_DROP_OUT_MIN_SCALE); 529 mBackgroundView.setFastAlpha((1f - t) * BACKGROUND_ALPHA); 530 mBackgroundView.fastInvalidate(); 531 mScreenshotContainerView.setFastAlpha(1f - t); 532 mScreenshotContainerView.setFastScaleX(scaleT); 533 mScreenshotContainerView.setFastScaleY(scaleT); 534 mScreenshotContainerView.fastInvalidate(); 535 mScreenshotLayout.invalidate(); 536 } 537 }); 538 } else { 539 // In the case where there is a status bar, animate to the origin of the bar (top-left) 540 final float scaleDurationPct = (float) SCREENSHOT_DROP_OUT_SCALE_DURATION 541 / SCREENSHOT_DROP_OUT_DURATION; 542 final Interpolator scaleInterpolator = new Interpolator() { 543 @Override 544 public float getInterpolation(float x) { 545 if (x < scaleDurationPct) { 546 // Decelerate, and scale the input accordingly 547 return (float) (1f - Math.pow(1f - (x / scaleDurationPct), 2f)); 548 } 549 return 1f; 550 } 551 }; 552 553 // Determine the bounds of how to scale 554 float halfScreenWidth = (w - 2f * mBgPadding) / 2f; 555 float halfScreenHeight = (h - 2f * mBgPadding) / 2f; 556 final float offsetPct = SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET; 557 final PointF finalPos = new PointF( 558 -halfScreenWidth + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenWidth, 559 -halfScreenHeight + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenHeight); 560 561 // Animate the screenshot to the status bar 562 anim.setDuration(SCREENSHOT_DROP_OUT_DURATION); 563 anim.addUpdateListener(new AnimatorUpdateListener() { 564 @Override 565 public void onAnimationUpdate(ValueAnimator animation) { 566 float t = ((Float) animation.getAnimatedValue()).floatValue(); 567 float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale) 568 - (float) scaleInterpolator.getInterpolation(t) 569 * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_DROP_OUT_MIN_SCALE); 570 mBackgroundView.setFastAlpha((1f - t) * BACKGROUND_ALPHA); 571 mBackgroundView.fastInvalidate(); 572 mScreenshotContainerView.setFastAlpha(1f - scaleInterpolator.getInterpolation(t)); 573 mScreenshotContainerView.setFastScaleX(scaleT); 574 mScreenshotContainerView.setFastScaleY(scaleT); 575 mScreenshotContainerView.setFastTranslationX(t * finalPos.x); 576 mScreenshotContainerView.setFastTranslationY(t * finalPos.y); 577 mScreenshotContainerView.fastInvalidate(); 578 mScreenshotLayout.invalidate(); 579 } 580 }); 581 } 582 return anim; 583 } 584 585 static void notifyScreenshotError(Context context, NotificationManager nManager) { 586 Resources r = context.getResources(); 587 588 // Clear all existing notification, compose the new notification and show it 589 Notification n = new Notification.Builder(context) 590 .setTicker(r.getString(R.string.screenshot_failed_title)) 591 .setContentTitle(r.getString(R.string.screenshot_failed_title)) 592 .setContentText(r.getString(R.string.screenshot_failed_text)) 593 .setSmallIcon(R.drawable.stat_notify_image_error) 594 .setWhen(System.currentTimeMillis()) 595 .setAutoCancel(true) 596 .getNotification(); 597 nManager.notify(SCREENSHOT_NOTIFICATION_ID, n); 598 } 599} 600