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