ViewImage.java revision 666ea1b28a76aeba74744148b15099254d918671
1/* 2 * Copyright (C) 2007 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.camera; 18 19import android.app.Activity; 20import android.content.Context; 21import android.content.Intent; 22import android.content.SharedPreferences; 23import android.graphics.Bitmap; 24import android.net.Uri; 25import android.os.Bundle; 26import android.preference.PreferenceManager; 27import android.provider.MediaStore; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.GestureDetector; 31import android.view.KeyEvent; 32import android.view.Menu; 33import android.view.MenuItem; 34import android.view.MotionEvent; 35import android.view.View; 36import android.view.Window; 37import android.view.WindowManager; 38import android.view.View.OnTouchListener; 39import android.view.animation.AlphaAnimation; 40import android.view.animation.Animation; 41import android.view.animation.AnimationUtils; 42import android.widget.Toast; 43import android.widget.ZoomButtonsController; 44 45import com.android.camera.gallery.IImage; 46import com.android.camera.gallery.IImageList; 47import com.android.camera.gallery.VideoObject; 48 49import java.util.Random; 50 51// This activity can display a whole picture and navigate them in a specific 52// gallery. It has two modes: normal mode and slide show mode. In normal mode 53// the user view one image at a time, and can click "previous" and "next" 54// button to see the previous or next image. In slide show mode it shows one 55// image after another, with some transition effect. 56public class ViewImage extends Activity implements View.OnClickListener { 57 private static final String PREF_SLIDESHOW_REPEAT = 58 "pref_gallery_slideshow_repeat_key"; 59 private static final String PREF_SHUFFLE_SLIDESHOW = 60 "pref_gallery_slideshow_shuffle_key"; 61 private static final String STATE_URI = "uri"; 62 private static final String STATE_SLIDESHOW = "slideshow"; 63 private static final String EXTRA_SLIDESHOW = "slideshow"; 64 private static final String TAG = "ViewImage"; 65 66 private ImageGetter mGetter; 67 private Uri mSavedUri; 68 boolean mPaused = true; 69 private boolean mShowControls = true; 70 71 // Choices for what adjacents to load. 72 private static final int[] sOrderAdjacents = new int[] {0, 1, -1}; 73 private static final int[] sOrderSlideshow = new int[] {0}; 74 75 final GetterHandler mHandler = new GetterHandler(); 76 77 private final Random mRandom = new Random(System.currentTimeMillis()); 78 private int [] mShuffleOrder = null; 79 private boolean mUseShuffleOrder = false; 80 private boolean mSlideShowLoop = false; 81 82 static final int MODE_NORMAL = 1; 83 static final int MODE_SLIDESHOW = 2; 84 private int mMode = MODE_NORMAL; 85 86 private boolean mFullScreenInNormalMode; 87 private boolean mShowActionIcons; 88 private View mActionIconPanel; 89 90 private int mSlideShowInterval; 91 private int mLastSlideShowImage; 92 int mCurrentPosition = 0; 93 94 // represents which style animation to use 95 private int mAnimationIndex; 96 private Animation [] mSlideShowInAnimation; 97 private Animation [] mSlideShowOutAnimation; 98 99 private SharedPreferences mPrefs; 100 101 private View mNextImageView; 102 private View mPrevImageView; 103 private final Animation mHideNextImageViewAnimation = 104 new AlphaAnimation(1F, 0F); 105 private final Animation mHidePrevImageViewAnimation = 106 new AlphaAnimation(1F, 0F); 107 private final Animation mShowNextImageViewAnimation = 108 new AlphaAnimation(0F, 1F); 109 private final Animation mShowPrevImageViewAnimation = 110 new AlphaAnimation(0F, 1F); 111 112 public static final String KEY_IMAGE_LIST = "image_list"; 113 private static final String STATE_SHOW_CONTROLS = "show_controls"; 114 115 IImageList mAllImages; 116 117 private ImageManager.ImageListParam mParam; 118 119 private int mSlideShowImageCurrent = 0; 120 private final ImageViewTouchBase [] mSlideShowImageViews = 121 new ImageViewTouchBase[2]; 122 123 GestureDetector mGestureDetector; 124 private ZoomButtonsController mZoomButtonsController; 125 126 // The image view displayed for normal mode. 127 private ImageViewTouch mImageView; 128 // This is the cache for thumbnail bitmaps. 129 private BitmapCache mCache; 130 private MenuHelper.MenuItemsResult mImageMenuRunnable; 131 private final Runnable mDismissOnScreenControlRunner = new Runnable() { 132 public void run() { 133 hideOnScreenControls(); 134 } 135 }; 136 137 private void updateNextPrevControls() { 138 boolean showPrev = mCurrentPosition > 0; 139 boolean showNext = mCurrentPosition < mAllImages.getCount() - 1; 140 141 boolean prevIsVisible = mPrevImageView.getVisibility() == View.VISIBLE; 142 boolean nextIsVisible = mNextImageView.getVisibility() == View.VISIBLE; 143 144 if (showPrev && !prevIsVisible) { 145 Animation a = mShowPrevImageViewAnimation; 146 a.setDuration(500); 147 mPrevImageView.startAnimation(a); 148 mPrevImageView.setVisibility(View.VISIBLE); 149 } else if (!showPrev && prevIsVisible) { 150 Animation a = mHidePrevImageViewAnimation; 151 a.setDuration(500); 152 mPrevImageView.startAnimation(a); 153 mPrevImageView.setVisibility(View.GONE); 154 } 155 156 if (showNext && !nextIsVisible) { 157 Animation a = mShowNextImageViewAnimation; 158 a.setDuration(500); 159 mNextImageView.startAnimation(a); 160 mNextImageView.setVisibility(View.VISIBLE); 161 } else if (!showNext && nextIsVisible) { 162 Animation a = mHideNextImageViewAnimation; 163 a.setDuration(500); 164 mNextImageView.startAnimation(a); 165 mNextImageView.setVisibility(View.GONE); 166 } 167 } 168 169 private void hideOnScreenControls() { 170 if (mShowActionIcons 171 && mActionIconPanel.getVisibility() == View.VISIBLE) { 172 Animation animation = new AlphaAnimation(1, 0); 173 animation.setDuration(500); 174 mActionIconPanel.startAnimation(animation); 175 mActionIconPanel.setVisibility(View.INVISIBLE); 176 } 177 178 if (mNextImageView.getVisibility() == View.VISIBLE) { 179 Animation a = mHideNextImageViewAnimation; 180 a.setDuration(500); 181 mNextImageView.startAnimation(a); 182 mNextImageView.setVisibility(View.INVISIBLE); 183 } 184 185 if (mPrevImageView.getVisibility() == View.VISIBLE) { 186 Animation a = mHidePrevImageViewAnimation; 187 a.setDuration(500); 188 mPrevImageView.startAnimation(a); 189 mPrevImageView.setVisibility(View.INVISIBLE); 190 } 191 192 mZoomButtonsController.setVisible(false); 193 } 194 195 private void showOnScreenControls() { 196 if (mPaused) return; 197 // If the view has not been attached to the window yet, the 198 // zoomButtonControls will not able to show up. So delay it until the 199 // view has attached to window. 200 if (mActionIconPanel.getWindowToken() == null) { 201 mHandler.postGetterCallback(new Runnable() { 202 public void run() { 203 showOnScreenControls(); 204 } 205 }); 206 return; 207 } 208 updateNextPrevControls(); 209 210 IImage image = mAllImages.getImageAt(mCurrentPosition); 211 if (image instanceof VideoObject) { 212 mZoomButtonsController.setVisible(false); 213 } else { 214 updateZoomButtonsEnabled(); 215 mZoomButtonsController.setVisible(true); 216 } 217 218 if (mShowActionIcons 219 && mActionIconPanel.getVisibility() != View.VISIBLE) { 220 Animation animation = new AlphaAnimation(0, 1); 221 animation.setDuration(500); 222 mActionIconPanel.startAnimation(animation); 223 mActionIconPanel.setVisibility(View.VISIBLE); 224 } 225 } 226 227 @Override 228 public boolean dispatchTouchEvent(MotionEvent m) { 229 if (mPaused) return true; 230 if (mZoomButtonsController.isVisible()) { 231 scheduleDismissOnScreenControls(); 232 } 233 return super.dispatchTouchEvent(m); 234 } 235 236 private void updateZoomButtonsEnabled() { 237 ImageViewTouch imageView = mImageView; 238 float scale = imageView.getScale(); 239 mZoomButtonsController.setZoomInEnabled(scale < imageView.mMaxZoom); 240 mZoomButtonsController.setZoomOutEnabled(scale > 1); 241 } 242 243 @Override 244 protected void onDestroy() { 245 // This is necessary to make the ZoomButtonsController unregister 246 // its configuration change receiver. 247 if (mZoomButtonsController != null) { 248 mZoomButtonsController.setVisible(false); 249 } 250 super.onDestroy(); 251 } 252 253 private void scheduleDismissOnScreenControls() { 254 mHandler.removeCallbacks(mDismissOnScreenControlRunner); 255 mHandler.postDelayed(mDismissOnScreenControlRunner, 2000); 256 } 257 258 private void setupOnScreenControls(View rootView, View ownerView) { 259 mNextImageView = rootView.findViewById(R.id.next_image); 260 mPrevImageView = rootView.findViewById(R.id.prev_image); 261 262 mNextImageView.setOnClickListener(this); 263 mPrevImageView.setOnClickListener(this); 264 265 setupZoomButtonController(ownerView); 266 setupOnTouchListeners(rootView); 267 } 268 269 private void setupZoomButtonController(final View ownerView) { 270 mZoomButtonsController = new ZoomButtonsController(ownerView); 271 mZoomButtonsController.setAutoDismissed(false); 272 mZoomButtonsController.setZoomSpeed(100); 273 mZoomButtonsController.setOnZoomListener( 274 new ZoomButtonsController.OnZoomListener() { 275 public void onVisibilityChanged(boolean visible) { 276 if (visible) { 277 updateZoomButtonsEnabled(); 278 } 279 } 280 281 public void onZoom(boolean zoomIn) { 282 if (zoomIn) { 283 mImageView.zoomIn(); 284 } else { 285 mImageView.zoomOut(); 286 } 287 mZoomButtonsController.setVisible(true); 288 updateZoomButtonsEnabled(); 289 } 290 }); 291 } 292 293 private void setupOnTouchListeners(View rootView) { 294 mGestureDetector = new GestureDetector(this, new MyGestureListener()); 295 296 // If the user touches anywhere on the panel (including the 297 // next/prev button). We show the on-screen controls. In addition 298 // to that, if the touch is not on the prev/next button, we 299 // pass the event to the gesture detector to detect double tap. 300 final OnTouchListener buttonListener = new OnTouchListener() { 301 public boolean onTouch(View v, MotionEvent event) { 302 scheduleDismissOnScreenControls(); 303 return false; 304 } 305 }; 306 307 OnTouchListener rootListener = new OnTouchListener() { 308 public boolean onTouch(View v, MotionEvent event) { 309 buttonListener.onTouch(v, event); 310 mGestureDetector.onTouchEvent(event); 311 312 // We do not use the return value of 313 // mGestureDetector.onTouchEvent because we will not receive 314 // the "up" event if we return false for the "down" event. 315 return true; 316 } 317 }; 318 319 mNextImageView.setOnTouchListener(buttonListener); 320 mPrevImageView.setOnTouchListener(buttonListener); 321 rootView.setOnTouchListener(rootListener); 322 } 323 324 private class MyGestureListener extends 325 GestureDetector.SimpleOnGestureListener { 326 327 @Override 328 public boolean onScroll(MotionEvent e1, MotionEvent e2, 329 float distanceX, float distanceY) { 330 ImageViewTouch imageView = mImageView; 331 if (imageView.getScale() > 1F) { 332 imageView.postTranslateCenter(-distanceX, -distanceY); 333 } 334 return true; 335 } 336 337 @Override 338 public boolean onSingleTapUp(MotionEvent e) { 339 setMode(MODE_NORMAL); 340 return true; 341 } 342 343 @Override 344 public boolean onSingleTapConfirmed(MotionEvent e) { 345 showOnScreenControls(); 346 scheduleDismissOnScreenControls(); 347 return true; 348 } 349 350 @Override 351 public boolean onDoubleTap(MotionEvent e) { 352 ImageViewTouch imageView = mImageView; 353 354 // Switch between the original scale and 3x scale. 355 if (imageView.getScale() > 2F) { 356 mImageView.zoomTo(1f); 357 } else { 358 mImageView.zoomToPoint(3f, e.getX(), e.getY()); 359 } 360 return true; 361 } 362 } 363 364 boolean isPickIntent() { 365 String action = getIntent().getAction(); 366 return (Intent.ACTION_PICK.equals(action) 367 || Intent.ACTION_GET_CONTENT.equals(action)); 368 } 369 370 @Override 371 public boolean onCreateOptionsMenu(Menu menu) { 372 super.onCreateOptionsMenu(menu); 373 374 MenuItem item = menu.add(Menu.NONE, Menu.NONE, 375 MenuHelper.POSITION_SLIDESHOW, 376 R.string.slide_show); 377 item.setOnMenuItemClickListener( 378 new MenuItem.OnMenuItemClickListener() { 379 public boolean onMenuItemClick(MenuItem item) { 380 setMode(MODE_SLIDESHOW); 381 mLastSlideShowImage = mCurrentPosition; 382 loadNextImage(mCurrentPosition, 0, true); 383 return true; 384 } 385 }); 386 item.setIcon(android.R.drawable.ic_menu_slideshow); 387 388 mImageMenuRunnable = MenuHelper.addImageMenuItems( 389 menu, 390 MenuHelper.INCLUDE_ALL, 391 ViewImage.this, 392 mHandler, 393 mDeletePhotoRunnable, 394 new MenuHelper.MenuInvoker() { 395 public void run(final MenuHelper.MenuCallback cb) { 396 if (mPaused) return; 397 setMode(MODE_NORMAL); 398 399 IImage image = mAllImages.getImageAt(mCurrentPosition); 400 Uri uri = image.fullSizeImageUri(); 401 cb.run(uri, image); 402 403 mImageView.clear(); 404 setImage(mCurrentPosition, false); 405 } 406 }); 407 408 item = menu.add(Menu.NONE, Menu.NONE, 409 MenuHelper.POSITION_GALLERY_SETTING, R.string.camerasettings); 410 item.setOnMenuItemClickListener( 411 new MenuItem.OnMenuItemClickListener() { 412 public boolean onMenuItemClick(MenuItem item) { 413 Intent preferences = new Intent(); 414 preferences.setClass(ViewImage.this, GallerySettings.class); 415 startActivity(preferences); 416 return true; 417 } 418 }); 419 item.setAlphabeticShortcut('p'); 420 item.setIcon(android.R.drawable.ic_menu_preferences); 421 422 return true; 423 } 424 425 protected Runnable mDeletePhotoRunnable = new Runnable() { 426 public void run() { 427 mAllImages.removeImageAt(mCurrentPosition); 428 if (mAllImages.getCount() == 0) { 429 finish(); 430 return; 431 } else { 432 if (mCurrentPosition == mAllImages.getCount()) { 433 mCurrentPosition -= 1; 434 } 435 } 436 mImageView.clear(); 437 mCache.clear(); // Because the position number is changed. 438 setImage(mCurrentPosition, true); 439 } 440 }; 441 442 @Override 443 public boolean onPrepareOptionsMenu(Menu menu) { 444 445 super.onPrepareOptionsMenu(menu); 446 if (mPaused) return false; 447 448 setMode(MODE_NORMAL); 449 IImage image = mAllImages.getImageAt(mCurrentPosition); 450 451 if (mImageMenuRunnable != null) { 452 mImageMenuRunnable.gettingReadyToOpen(menu, image); 453 } 454 455 Uri uri = mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri(); 456 MenuHelper.enableShareMenuItem(menu, MenuHelper.isWhiteListUri(uri)); 457 458 MenuHelper.enableShowOnMapMenuItem(menu, MenuHelper.hasLatLngData(image)); 459 460 return true; 461 } 462 463 @Override 464 public boolean onMenuItemSelected(int featureId, MenuItem item) { 465 boolean b = super.onMenuItemSelected(featureId, item); 466 if (mImageMenuRunnable != null) { 467 mImageMenuRunnable.aboutToCall(item, 468 mAllImages.getImageAt(mCurrentPosition)); 469 } 470 return b; 471 } 472 473 void setImage(int pos, boolean showControls) { 474 mCurrentPosition = pos; 475 476 Bitmap b = mCache.getBitmap(pos); 477 if (b != null) { 478 IImage image = mAllImages.getImageAt(pos); 479 mImageView.setImageRotateBitmapResetBase( 480 new RotateBitmap(b, image.getDegreesRotated()), true); 481 updateZoomButtonsEnabled(); 482 } 483 484 ImageGetterCallback cb = new ImageGetterCallback() { 485 public void completed() { 486 } 487 488 public boolean wantsThumbnail(int pos, int offset) { 489 return !mCache.hasBitmap(pos + offset); 490 } 491 492 public boolean wantsFullImage(int pos, int offset) { 493 return offset == 0; 494 } 495 496 public int fullImageSizeToUse(int pos, int offset) { 497 // this number should be bigger so that we can zoom. we may 498 // need to get fancier and read in the fuller size image as the 499 // user starts to zoom. 500 // Originally the value is set to 480 in order to avoid OOM. 501 // Now we set it to 2048 because of using 502 // native memory allocation for Bitmaps. 503 final int imageViewSize = 2048; 504 return imageViewSize; 505 } 506 507 public int [] loadOrder() { 508 return sOrderAdjacents; 509 } 510 511 public void imageLoaded(int pos, int offset, RotateBitmap bitmap, 512 boolean isThumb) { 513 // shouldn't get here after onPause() 514 515 // We may get a result from a previous request. Ignore it. 516 if (pos != mCurrentPosition) { 517 bitmap.recycle(); 518 return; 519 } 520 521 if (isThumb) { 522 mCache.put(pos + offset, bitmap.getBitmap()); 523 } 524 if (offset == 0) { 525 // isThumb: We always load thumb bitmap first, so we will 526 // reset the supp matrix for then thumb bitmap, and keep 527 // the supp matrix when the full bitmap is loaded. 528 mImageView.setImageRotateBitmapResetBase(bitmap, isThumb); 529 updateZoomButtonsEnabled(); 530 } 531 } 532 }; 533 534 // Could be null if we're stopping a slide show in the course of pausing 535 if (mGetter != null) { 536 mGetter.setPosition(pos, cb, mAllImages, mHandler); 537 } 538 updateActionIcons(); 539 if (showControls) showOnScreenControls(); 540 scheduleDismissOnScreenControls(); 541 } 542 543 @Override 544 public void onCreate(Bundle instanceState) { 545 super.onCreate(instanceState); 546 547 Intent intent = getIntent(); 548 mFullScreenInNormalMode = intent.getBooleanExtra( 549 MediaStore.EXTRA_FULL_SCREEN, true); 550 mShowActionIcons = intent.getBooleanExtra( 551 MediaStore.EXTRA_SHOW_ACTION_ICONS, true); 552 553 mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 554 555 setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); 556 requestWindowFeature(Window.FEATURE_NO_TITLE); 557 setContentView(R.layout.viewimage); 558 559 mImageView = (ImageViewTouch) findViewById(R.id.image); 560 mImageView.setEnableTrackballScroll(true); 561 mCache = new BitmapCache(3); 562 mImageView.setRecycler(mCache); 563 564 makeGetter(); 565 566 mAnimationIndex = -1; 567 568 mSlideShowInAnimation = new Animation[] { 569 makeInAnimation(R.anim.transition_in), 570 makeInAnimation(R.anim.slide_in), 571 makeInAnimation(R.anim.slide_in_vertical), 572 }; 573 574 mSlideShowOutAnimation = new Animation[] { 575 makeOutAnimation(R.anim.transition_out), 576 makeOutAnimation(R.anim.slide_out), 577 makeOutAnimation(R.anim.slide_out_vertical), 578 }; 579 580 mSlideShowImageViews[0] = 581 (ImageViewTouchBase) findViewById(R.id.image1_slideShow); 582 mSlideShowImageViews[1] = 583 (ImageViewTouchBase) findViewById(R.id.image2_slideShow); 584 for (ImageViewTouchBase v : mSlideShowImageViews) { 585 v.setVisibility(View.INVISIBLE); 586 v.setRecycler(mCache); 587 } 588 589 mActionIconPanel = findViewById(R.id.action_icon_panel); 590 591 mParam = getIntent().getParcelableExtra(KEY_IMAGE_LIST); 592 593 boolean slideshow; 594 if (instanceState != null) { 595 mSavedUri = instanceState.getParcelable(STATE_URI); 596 slideshow = instanceState.getBoolean(STATE_SLIDESHOW, false); 597 mShowControls = instanceState.getBoolean(STATE_SHOW_CONTROLS, true); 598 } else { 599 mSavedUri = getIntent().getData(); 600 slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false); 601 } 602 603 // We only show action icons for URIs that we know we can share and 604 // delete. Although we get read permission (for the images) from 605 // applications like MMS, we cannot pass the permission to other 606 // activities due to the current framework design. 607 if (!MenuHelper.isWhiteListUri(mSavedUri)) { 608 mShowActionIcons = false; 609 } 610 611 if (mShowActionIcons) { 612 int[] pickIds = {R.id.attach, R.id.cancel}; 613 int[] normalIds = {R.id.setas, R.id.play, R.id.share, R.id.discard}; 614 int[] connectIds = isPickIntent() ? pickIds : normalIds; 615 for (int id : connectIds) { 616 View view = mActionIconPanel.findViewById(id); 617 view.setVisibility(View.VISIBLE); 618 view.setOnClickListener(this); 619 } 620 } 621 622 // Don't show the "delete" icon for SingleImageList. 623 if (ImageManager.isSingleImageMode(mSavedUri.toString())) { 624 mActionIconPanel.findViewById(R.id.discard) 625 .setVisibility(View.GONE); 626 } 627 628 if (slideshow) { 629 setMode(MODE_SLIDESHOW); 630 } else { 631 if (mFullScreenInNormalMode) { 632 getWindow().addFlags( 633 WindowManager.LayoutParams.FLAG_FULLSCREEN); 634 } 635 if (mShowActionIcons) { 636 mActionIconPanel.setVisibility(View.VISIBLE); 637 } 638 } 639 640 setupOnScreenControls(findViewById(R.id.rootLayout), mImageView); 641 } 642 643 private void updateActionIcons() { 644 if (isPickIntent()) return; 645 646 IImage image = mAllImages.getImageAt(mCurrentPosition); 647 View panel = mActionIconPanel; 648 if (image instanceof VideoObject) { 649 panel.findViewById(R.id.setas).setVisibility(View.GONE); 650 panel.findViewById(R.id.play).setVisibility(View.VISIBLE); 651 } else { 652 panel.findViewById(R.id.setas).setVisibility(View.VISIBLE); 653 panel.findViewById(R.id.play).setVisibility(View.GONE); 654 } 655 } 656 657 private Animation makeInAnimation(int id) { 658 Animation inAnimation = AnimationUtils.loadAnimation(this, id); 659 return inAnimation; 660 } 661 662 private Animation makeOutAnimation(int id) { 663 Animation outAnimation = AnimationUtils.loadAnimation(this, id); 664 return outAnimation; 665 } 666 667 private static int getPreferencesInteger( 668 SharedPreferences prefs, String key, int defaultValue) { 669 String value = prefs.getString(key, null); 670 try { 671 return value == null ? defaultValue : Integer.parseInt(value); 672 } catch (NumberFormatException ex) { 673 Log.e(TAG, "couldn't parse preference: " + value, ex); 674 return defaultValue; 675 } 676 } 677 678 void setMode(int mode) { 679 if (mMode == mode) { 680 return; 681 } 682 View slideshowPanel = findViewById(R.id.slideShowContainer); 683 View normalPanel = findViewById(R.id.abs); 684 685 Window win = getWindow(); 686 mMode = mode; 687 if (mode == MODE_SLIDESHOW) { 688 slideshowPanel.setVisibility(View.VISIBLE); 689 normalPanel.setVisibility(View.GONE); 690 691 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN 692 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 693 694 mImageView.clear(); 695 mActionIconPanel.setVisibility(View.GONE); 696 697 slideshowPanel.getRootView().requestLayout(); 698 699 // The preferences we want to read: 700 // mUseShuffleOrder 701 // mSlideShowLoop 702 // mAnimationIndex 703 // mSlideShowInterval 704 705 mUseShuffleOrder = mPrefs.getBoolean(PREF_SHUFFLE_SLIDESHOW, false); 706 mSlideShowLoop = mPrefs.getBoolean(PREF_SLIDESHOW_REPEAT, false); 707 mAnimationIndex = getPreferencesInteger( 708 mPrefs, "pref_gallery_slideshow_transition_key", 0); 709 mSlideShowInterval = getPreferencesInteger( 710 mPrefs, "pref_gallery_slideshow_interval_key", 3) * 1000; 711 } else { 712 slideshowPanel.setVisibility(View.GONE); 713 normalPanel.setVisibility(View.VISIBLE); 714 715 win.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 716 if (mFullScreenInNormalMode) { 717 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 718 } else { 719 win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 720 } 721 722 if (mGetter != null) { 723 mGetter.cancelCurrent(); 724 } 725 726 if (mShowActionIcons) { 727 Animation animation = new AlphaAnimation(0F, 1F); 728 animation.setDuration(500); 729 mActionIconPanel.setAnimation(animation); 730 mActionIconPanel.setVisibility(View.VISIBLE); 731 } 732 733 ImageViewTouchBase dst = mImageView; 734 dst.mLastXTouchPos = -1; 735 dst.mLastYTouchPos = -1; 736 737 for (ImageViewTouchBase ivt : mSlideShowImageViews) { 738 ivt.clear(); 739 } 740 741 mShuffleOrder = null; 742 743 // mGetter null is a proxy for being paused 744 if (mGetter != null) { 745 setImage(mCurrentPosition, true); 746 } 747 } 748 } 749 750 private void generateShuffleOrder() { 751 if (mShuffleOrder == null 752 || mShuffleOrder.length != mAllImages.getCount()) { 753 mShuffleOrder = new int[mAllImages.getCount()]; 754 for (int i = 0, n = mShuffleOrder.length; i < n; i++) { 755 mShuffleOrder[i] = i; 756 } 757 } 758 759 for (int i = mShuffleOrder.length - 1; i >= 0; i--) { 760 int r = mRandom.nextInt(i + 1); 761 if (r != i) { 762 int tmp = mShuffleOrder[r]; 763 mShuffleOrder[r] = mShuffleOrder[i]; 764 mShuffleOrder[i] = tmp; 765 } 766 } 767 } 768 769 private void loadNextImage(final int requestedPos, final long delay, 770 final boolean firstCall) { 771 if (firstCall && mUseShuffleOrder) { 772 generateShuffleOrder(); 773 } 774 775 final long targetDisplayTime = System.currentTimeMillis() + delay; 776 777 ImageGetterCallback cb = new ImageGetterCallback() { 778 public void completed() { 779 } 780 781 public boolean wantsThumbnail(int pos, int offset) { 782 return true; 783 } 784 785 public boolean wantsFullImage(int pos, int offset) { 786 return false; 787 } 788 789 public int [] loadOrder() { 790 return sOrderSlideshow; 791 } 792 793 public int fullImageSizeToUse(int pos, int offset) { 794 return 480; // TODO compute this 795 } 796 797 public void imageLoaded(final int pos, final int offset, 798 final RotateBitmap bitmap, final boolean isThumb) { 799 long timeRemaining = Math.max(0, 800 targetDisplayTime - System.currentTimeMillis()); 801 mHandler.postDelayedGetterCallback(new Runnable() { 802 public void run() { 803 if (mMode == MODE_NORMAL) { 804 return; 805 } 806 807 ImageViewTouchBase oldView = 808 mSlideShowImageViews[mSlideShowImageCurrent]; 809 810 if (++mSlideShowImageCurrent 811 == mSlideShowImageViews.length) { 812 mSlideShowImageCurrent = 0; 813 } 814 815 ImageViewTouchBase newView = 816 mSlideShowImageViews[mSlideShowImageCurrent]; 817 newView.setVisibility(View.VISIBLE); 818 newView.setImageRotateBitmapResetBase(bitmap, true); 819 newView.bringToFront(); 820 821 int animation = 0; 822 823 if (mAnimationIndex == -1) { 824 int n = mRandom.nextInt( 825 mSlideShowInAnimation.length); 826 animation = n; 827 } else { 828 animation = mAnimationIndex; 829 } 830 831 Animation aIn = mSlideShowInAnimation[animation]; 832 newView.startAnimation(aIn); 833 newView.setVisibility(View.VISIBLE); 834 835 Animation aOut = mSlideShowOutAnimation[animation]; 836 oldView.setVisibility(View.INVISIBLE); 837 oldView.startAnimation(aOut); 838 839 mCurrentPosition = requestedPos; 840 841 if (mCurrentPosition == mLastSlideShowImage 842 && !firstCall) { 843 if (mSlideShowLoop) { 844 if (mUseShuffleOrder) { 845 generateShuffleOrder(); 846 } 847 } else { 848 setMode(MODE_NORMAL); 849 return; 850 } 851 } 852 853 loadNextImage( 854 (mCurrentPosition + 1) % mAllImages.getCount(), 855 mSlideShowInterval, false); 856 } 857 }, timeRemaining); 858 } 859 }; 860 // Could be null if we're stopping a slide show in the course of pausing 861 if (mGetter != null) { 862 int pos = requestedPos; 863 if (mShuffleOrder != null) { 864 pos = mShuffleOrder[pos]; 865 } 866 mGetter.setPosition(pos, cb, mAllImages, mHandler); 867 } 868 } 869 870 private void makeGetter() { 871 mGetter = new ImageGetter(getContentResolver()); 872 } 873 874 private IImageList buildImageListFromUri(Uri uri) { 875 String sortOrder = mPrefs.getString( 876 "pref_gallery_sort_key", "descending"); 877 int sort = sortOrder.equals("ascending") 878 ? ImageManager.SORT_ASCENDING 879 : ImageManager.SORT_DESCENDING; 880 return ImageManager.makeImageList(getContentResolver(), uri, sort); 881 } 882 883 private boolean init(Uri uri) { 884 if (uri == null) return false; 885 mAllImages = (mParam == null) 886 ? buildImageListFromUri(uri) 887 : ImageManager.makeImageList(getContentResolver(), mParam); 888 IImage image = mAllImages.getImageForUri(uri); 889 if (image == null) return false; 890 mCurrentPosition = mAllImages.getImageIndex(image); 891 mLastSlideShowImage = mCurrentPosition; 892 return true; 893 } 894 895 private Uri getCurrentUri() { 896 if (mAllImages.getCount() == 0) return null; 897 IImage image = mAllImages.getImageAt(mCurrentPosition); 898 return image.fullSizeImageUri(); 899 } 900 901 @Override 902 public void onSaveInstanceState(Bundle b) { 903 super.onSaveInstanceState(b); 904 b.putParcelable(STATE_URI, 905 mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri()); 906 b.putBoolean(STATE_SLIDESHOW, mMode == MODE_SLIDESHOW); 907 } 908 909 @Override 910 public void onStart() { 911 super.onStart(); 912 mPaused = false; 913 914 if (!init(mSavedUri)) { 915 Log.w(TAG, "init failed: " + mSavedUri); 916 finish(); 917 return; 918 } 919 920 // normally this will never be zero but if one "backs" into this 921 // activity after removing the sdcard it could be zero. in that 922 // case just "finish" since there's nothing useful that can happen. 923 int count = mAllImages.getCount(); 924 if (count == 0) { 925 finish(); 926 return; 927 } else if (count <= mCurrentPosition) { 928 mCurrentPosition = count - 1; 929 } 930 931 if (mGetter == null) { 932 makeGetter(); 933 } 934 935 if (mMode == MODE_SLIDESHOW) { 936 loadNextImage(mCurrentPosition, 0, true); 937 } else { // MODE_NORMAL 938 setImage(mCurrentPosition, mShowControls); 939 mShowControls = false; 940 } 941 } 942 943 @Override 944 public void onStop() { 945 super.onStop(); 946 mPaused = true; 947 948 // mGetter could be null if we call finish() and leave early in 949 // onStart(). 950 if (mGetter != null) { 951 mGetter.cancelCurrent(); 952 mGetter.stop(); 953 mGetter = null; 954 } 955 setMode(MODE_NORMAL); 956 957 // removing all callback in the message queue 958 mHandler.removeAllGetterCallbacks(); 959 960 if (mAllImages != null) { 961 mSavedUri = getCurrentUri(); 962 mAllImages.close(); 963 mAllImages = null; 964 } 965 966 hideOnScreenControls(); 967 mImageView.clear(); 968 mCache.clear(); 969 970 for (ImageViewTouchBase iv : mSlideShowImageViews) { 971 iv.clear(); 972 } 973 } 974 975 private void startShareMediaActivity(IImage image) { 976 boolean isVideo = image instanceof VideoObject; 977 Intent intent = new Intent(); 978 intent.setAction(Intent.ACTION_SEND); 979 intent.setType(image.getMimeType()); 980 intent.putExtra(Intent.EXTRA_STREAM, image.fullSizeImageUri()); 981 try { 982 startActivity(Intent.createChooser(intent, getText( 983 isVideo ? R.string.sendVideo : R.string.sendImage))); 984 } catch (android.content.ActivityNotFoundException ex) { 985 Toast.makeText(this, isVideo 986 ? R.string.no_way_to_share_image 987 : R.string.no_way_to_share_video, 988 Toast.LENGTH_SHORT).show(); 989 } 990 } 991 992 private void startPlayVideoActivity() { 993 IImage image = mAllImages.getImageAt(mCurrentPosition); 994 Intent intent = new Intent( 995 Intent.ACTION_VIEW, image.fullSizeImageUri()); 996 try { 997 startActivity(intent); 998 } catch (android.content.ActivityNotFoundException ex) { 999 Log.e(TAG, "Couldn't view video " + image.fullSizeImageUri(), ex); 1000 } 1001 } 1002 1003 public void onClick(View v) { 1004 switch (v.getId()) { 1005 case R.id.discard: 1006 MenuHelper.deletePhoto(this, mDeletePhotoRunnable); 1007 break; 1008 case R.id.play: 1009 startPlayVideoActivity(); 1010 break; 1011 case R.id.share: { 1012 IImage image = mAllImages.getImageAt(mCurrentPosition); 1013 if (!MenuHelper.isWhiteListUri(image.fullSizeImageUri())) { 1014 return; 1015 } 1016 startShareMediaActivity(image); 1017 break; 1018 } 1019 case R.id.setas: { 1020 IImage image = mAllImages.getImageAt(mCurrentPosition); 1021 Intent intent = Util.createSetAsIntent(image); 1022 try { 1023 startActivity(Intent.createChooser( 1024 intent, getText(R.string.setImage))); 1025 } catch (android.content.ActivityNotFoundException ex) { 1026 Toast.makeText(this, R.string.no_way_to_share_video, 1027 Toast.LENGTH_SHORT).show(); 1028 } 1029 break; 1030 } 1031 case R.id.next_image: 1032 moveNextOrPrevious(1); 1033 break; 1034 case R.id.prev_image: 1035 moveNextOrPrevious(-1); 1036 break; 1037 } 1038 } 1039 1040 private void moveNextOrPrevious(int delta) { 1041 int nextImagePos = mCurrentPosition + delta; 1042 if ((0 <= nextImagePos) && (nextImagePos < mAllImages.getCount())) { 1043 setImage(nextImagePos, true); 1044 showOnScreenControls(); 1045 } 1046 } 1047 1048 @Override 1049 protected void onActivityResult(int requestCode, int resultCode, 1050 Intent data) { 1051 switch (requestCode) { 1052 case MenuHelper.RESULT_COMMON_MENU_CROP: 1053 if (resultCode == RESULT_OK) { 1054 // The CropImage activity passes back the Uri of the 1055 // cropped image as the Action rather than the Data. 1056 mSavedUri = Uri.parse(data.getAction()); 1057 1058 // if onStart() runs before, then set the returned 1059 // image as currentImage. 1060 if (mAllImages != null) { 1061 IImage image = mAllImages.getImageForUri(mSavedUri); 1062 // image could be null if SD card is removed. 1063 if (image == null) { 1064 finish(); 1065 } else { 1066 mCurrentPosition = mAllImages.getImageIndex(image); 1067 setImage(mCurrentPosition, false); 1068 } 1069 } 1070 } 1071 break; 1072 } 1073 } 1074} 1075 1076class ImageViewTouch extends ImageViewTouchBase { 1077 private final ViewImage mViewImage; 1078 private boolean mEnableTrackballScroll; 1079 1080 public ImageViewTouch(Context context) { 1081 super(context); 1082 mViewImage = (ViewImage) context; 1083 } 1084 1085 public ImageViewTouch(Context context, AttributeSet attrs) { 1086 super(context, attrs); 1087 mViewImage = (ViewImage) context; 1088 } 1089 1090 public void setEnableTrackballScroll(boolean enable) { 1091 mEnableTrackballScroll = enable; 1092 } 1093 1094 protected void postTranslateCenter(float dx, float dy) { 1095 super.postTranslate(dx, dy); 1096 center(true, true); 1097 } 1098 1099 private static final float PAN_RATE = 20; 1100 1101 // This is the time we allow the dpad to change the image position again. 1102 private long mNextChangePositionTime; 1103 1104 @Override 1105 public boolean onKeyDown(int keyCode, KeyEvent event) { 1106 if (mViewImage.mPaused) return false; 1107 1108 // Don't respond to arrow keys if trackball scrolling is not enabled 1109 if (!mEnableTrackballScroll) { 1110 if ((keyCode >= KeyEvent.KEYCODE_DPAD_UP) 1111 && (keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT)) { 1112 return super.onKeyDown(keyCode, event); 1113 } 1114 } 1115 1116 int current = mViewImage.mCurrentPosition; 1117 1118 int nextImagePos = -2; // default no next image 1119 try { 1120 switch (keyCode) { 1121 case KeyEvent.KEYCODE_DPAD_CENTER: { 1122 if (mViewImage.isPickIntent()) { 1123 IImage img = mViewImage.mAllImages 1124 .getImageAt(mViewImage.mCurrentPosition); 1125 mViewImage.setResult(ViewImage.RESULT_OK, 1126 new Intent().setData(img.fullSizeImageUri())); 1127 mViewImage.finish(); 1128 } 1129 break; 1130 } 1131 case KeyEvent.KEYCODE_DPAD_LEFT: { 1132 if (getScale() <= 1F && event.getEventTime() 1133 >= mNextChangePositionTime) { 1134 nextImagePos = current - 1; 1135 mNextChangePositionTime = event.getEventTime() + 500; 1136 } else { 1137 panBy(PAN_RATE, 0); 1138 center(true, false); 1139 } 1140 return true; 1141 } 1142 case KeyEvent.KEYCODE_DPAD_RIGHT: { 1143 if (getScale() <= 1F && event.getEventTime() 1144 >= mNextChangePositionTime) { 1145 nextImagePos = current + 1; 1146 mNextChangePositionTime = event.getEventTime() + 500; 1147 } else { 1148 panBy(-PAN_RATE, 0); 1149 center(true, false); 1150 } 1151 return true; 1152 } 1153 case KeyEvent.KEYCODE_DPAD_UP: { 1154 panBy(0, PAN_RATE); 1155 center(false, true); 1156 return true; 1157 } 1158 case KeyEvent.KEYCODE_DPAD_DOWN: { 1159 panBy(0, -PAN_RATE); 1160 center(false, true); 1161 return true; 1162 } 1163 case KeyEvent.KEYCODE_DEL: 1164 MenuHelper.deletePhoto( 1165 mViewImage, mViewImage.mDeletePhotoRunnable); 1166 break; 1167 } 1168 } finally { 1169 if (nextImagePos >= 0 1170 && nextImagePos < mViewImage.mAllImages.getCount()) { 1171 synchronized (mViewImage) { 1172 mViewImage.setMode(ViewImage.MODE_NORMAL); 1173 mViewImage.setImage(nextImagePos, true); 1174 } 1175 } else if (nextImagePos != -2) { 1176 center(true, true); 1177 } 1178 } 1179 1180 return super.onKeyDown(keyCode, event); 1181 } 1182} 1183 1184// This is a cache for Bitmap displayed in ViewImage (normal mode, thumb only). 1185class BitmapCache implements ImageViewTouchBase.Recycler { 1186 public static class Entry { 1187 int mPos; 1188 Bitmap mBitmap; 1189 public Entry() { 1190 clear(); 1191 } 1192 public void clear() { 1193 mPos = -1; 1194 mBitmap = null; 1195 } 1196 } 1197 1198 private final Entry[] mCache; 1199 1200 public BitmapCache(int size) { 1201 mCache = new Entry[size]; 1202 for (int i = 0; i < mCache.length; i++) { 1203 mCache[i] = new Entry(); 1204 } 1205 } 1206 1207 // Given the position, find the associated entry. Returns null if there is 1208 // no such entry. 1209 private Entry findEntry(int pos) { 1210 for (Entry e : mCache) { 1211 if (pos == e.mPos) { 1212 return e; 1213 } 1214 } 1215 return null; 1216 } 1217 1218 // Returns the thumb bitmap if we have it, otherwise return null. 1219 public synchronized Bitmap getBitmap(int pos) { 1220 Entry e = findEntry(pos); 1221 if (e != null) { 1222 return e.mBitmap; 1223 } 1224 return null; 1225 } 1226 1227 public synchronized void put(int pos, Bitmap bitmap) { 1228 // First see if we already have this entry. 1229 if (findEntry(pos) != null) { 1230 return; 1231 } 1232 1233 // Find the best entry we should replace. 1234 // See if there is any empty entry. 1235 // Otherwise assuming sequential access, kick out the entry with the 1236 // greatest distance. 1237 Entry best = null; 1238 int maxDist = -1; 1239 for (Entry e : mCache) { 1240 if (e.mPos == -1) { 1241 best = e; 1242 break; 1243 } else { 1244 int dist = Math.abs(pos - e.mPos); 1245 if (dist > maxDist) { 1246 maxDist = dist; 1247 best = e; 1248 } 1249 } 1250 } 1251 1252 // Recycle the image being kicked out. 1253 // This only works because our current usage is sequential, so we 1254 // do not happen to recycle the image being displayed. 1255 if (best.mBitmap != null) { 1256 best.mBitmap.recycle(); 1257 } 1258 1259 best.mPos = pos; 1260 best.mBitmap = bitmap; 1261 } 1262 1263 // Recycle all bitmaps in the cache and clear the cache. 1264 public synchronized void clear() { 1265 for (Entry e : mCache) { 1266 if (e.mBitmap != null) { 1267 e.mBitmap.recycle(); 1268 } 1269 e.clear(); 1270 } 1271 } 1272 1273 // Returns whether the bitmap is in the cache. 1274 public synchronized boolean hasBitmap(int pos) { 1275 Entry e = findEntry(pos); 1276 return (e != null); 1277 } 1278 1279 // Recycle the bitmap if it's not in the cache. 1280 // The input must be non-null. 1281 public synchronized void recycle(Bitmap b) { 1282 for (Entry e : mCache) { 1283 if (e.mPos != -1) { 1284 if (e.mBitmap == b) { 1285 return; 1286 } 1287 } 1288 } 1289 b.recycle(); 1290 } 1291} 1292