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