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