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