ViewImage.java revision d6c2fb7a38fcdb58742fcfffd67a4594487ec71c
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 Activity 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 ImageViewTouch imageView = mImageView; 333 if (imageView.getScale() > 1F) { 334 imageView.postTranslateCenter(-distanceX, -distanceY); 335 } 336 return true; 337 } 338 339 @Override 340 public boolean onSingleTapUp(MotionEvent e) { 341 setMode(MODE_NORMAL); 342 return true; 343 } 344 345 @Override 346 public boolean onSingleTapConfirmed(MotionEvent e) { 347 showOnScreenControls(); 348 scheduleDismissOnScreenControls(); 349 return true; 350 } 351 352 @Override 353 public boolean onDoubleTap(MotionEvent e) { 354 ImageViewTouch imageView = mImageView; 355 356 // Switch between the original scale and 3x scale. 357 if (imageView.getScale() > 2F) { 358 mImageView.zoomTo(1f); 359 } else { 360 mImageView.zoomToPoint(3f, e.getX(), e.getY()); 361 } 362 return true; 363 } 364 } 365 366 boolean isPickIntent() { 367 String action = getIntent().getAction(); 368 return (Intent.ACTION_PICK.equals(action) 369 || Intent.ACTION_GET_CONTENT.equals(action)); 370 } 371 372 @Override 373 public boolean onCreateOptionsMenu(Menu menu) { 374 super.onCreateOptionsMenu(menu); 375 376 MenuItem item = menu.add(Menu.NONE, Menu.NONE, 377 MenuHelper.POSITION_SLIDESHOW, 378 R.string.slide_show); 379 item.setOnMenuItemClickListener( 380 new MenuItem.OnMenuItemClickListener() { 381 public boolean onMenuItemClick(MenuItem item) { 382 setMode(MODE_SLIDESHOW); 383 mLastSlideShowImage = mCurrentPosition; 384 loadNextImage(mCurrentPosition, 0, true); 385 return true; 386 } 387 }); 388 item.setIcon(android.R.drawable.ic_menu_slideshow); 389 390 mImageMenuRunnable = MenuHelper.addImageMenuItems( 391 menu, 392 MenuHelper.INCLUDE_ALL, 393 ViewImage.this, 394 mHandler, 395 mDeletePhotoRunnable, 396 new MenuHelper.MenuInvoker() { 397 public void run(final MenuHelper.MenuCallback cb) { 398 if (mPaused) return; 399 setMode(MODE_NORMAL); 400 401 IImage image = mAllImages.getImageAt(mCurrentPosition); 402 Uri uri = image.fullSizeImageUri(); 403 cb.run(uri, image); 404 405 mImageView.clear(); 406 setImage(mCurrentPosition, false); 407 } 408 }); 409 410 item = menu.add(Menu.NONE, Menu.NONE, 411 MenuHelper.POSITION_GALLERY_SETTING, R.string.camerasettings); 412 item.setOnMenuItemClickListener( 413 new MenuItem.OnMenuItemClickListener() { 414 public boolean onMenuItemClick(MenuItem item) { 415 Intent preferences = new Intent(); 416 preferences.setClass(ViewImage.this, GallerySettings.class); 417 startActivity(preferences); 418 return true; 419 } 420 }); 421 item.setAlphabeticShortcut('p'); 422 item.setIcon(android.R.drawable.ic_menu_preferences); 423 424 return true; 425 } 426 427 protected Runnable mDeletePhotoRunnable = new Runnable() { 428 public void run() { 429 mAllImages.removeImageAt(mCurrentPosition); 430 if (mAllImages.getCount() == 0) { 431 finish(); 432 return; 433 } else { 434 if (mCurrentPosition == mAllImages.getCount()) { 435 mCurrentPosition -= 1; 436 } 437 } 438 mImageView.clear(); 439 mCache.clear(); // Because the position number is changed. 440 setImage(mCurrentPosition, true); 441 } 442 }; 443 444 @Override 445 public boolean onPrepareOptionsMenu(Menu menu) { 446 447 super.onPrepareOptionsMenu(menu); 448 if (mPaused) return false; 449 450 setMode(MODE_NORMAL); 451 IImage image = mAllImages.getImageAt(mCurrentPosition); 452 453 if (mImageMenuRunnable != null) { 454 mImageMenuRunnable.gettingReadyToOpen(menu, image); 455 } 456 457 Uri uri = mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri(); 458 MenuHelper.enableShareMenuItem(menu, MenuHelper.isWhiteListUri(uri)); 459 460 MenuHelper.enableShowOnMapMenuItem(menu, MenuHelper.hasLatLngData(image)); 461 462 return true; 463 } 464 465 @Override 466 public boolean onMenuItemSelected(int featureId, MenuItem item) { 467 boolean b = super.onMenuItemSelected(featureId, item); 468 if (mImageMenuRunnable != null) { 469 mImageMenuRunnable.aboutToCall(item, 470 mAllImages.getImageAt(mCurrentPosition)); 471 } 472 return b; 473 } 474 475 void setImage(int pos, boolean showControls) { 476 mCurrentPosition = pos; 477 478 Bitmap b = mCache.getBitmap(pos); 479 if (b != null) { 480 IImage image = mAllImages.getImageAt(pos); 481 mImageView.setImageRotateBitmapResetBase( 482 new RotateBitmap(b, image.getDegreesRotated()), true); 483 updateZoomButtonsEnabled(); 484 } 485 486 ImageGetterCallback cb = new ImageGetterCallback() { 487 public void completed() { 488 } 489 490 public boolean wantsThumbnail(int pos, int offset) { 491 return !mCache.hasBitmap(pos + offset); 492 } 493 494 public boolean wantsFullImage(int pos, int offset) { 495 return offset == 0; 496 } 497 498 public int fullImageSizeToUse(int pos, int offset) { 499 // this number should be bigger so that we can zoom. we may 500 // need to get fancier and read in the fuller size image as the 501 // user starts to zoom. 502 // Originally the value is set to 480 in order to avoid OOM. 503 // Now we set it to 2048 because of using 504 // native memory allocation for Bitmaps. 505 final int imageViewSize = 2048; 506 return imageViewSize; 507 } 508 509 public int [] loadOrder() { 510 return sOrderAdjacents; 511 } 512 513 public void imageLoaded(int pos, int offset, RotateBitmap bitmap, 514 boolean isThumb) { 515 // shouldn't get here after onPause() 516 517 // We may get a result from a previous request. Ignore it. 518 if (pos != mCurrentPosition) { 519 bitmap.recycle(); 520 return; 521 } 522 523 if (isThumb) { 524 mCache.put(pos + offset, bitmap.getBitmap()); 525 } 526 if (offset == 0) { 527 // isThumb: We always load thumb bitmap first, so we will 528 // reset the supp matrix for then thumb bitmap, and keep 529 // the supp matrix when the full bitmap is loaded. 530 mImageView.setImageRotateBitmapResetBase(bitmap, isThumb); 531 updateZoomButtonsEnabled(); 532 } 533 } 534 }; 535 536 // Could be null if we're stopping a slide show in the course of pausing 537 if (mGetter != null) { 538 mGetter.setPosition(pos, cb, mAllImages, mHandler); 539 } 540 updateActionIcons(); 541 if (showControls) showOnScreenControls(); 542 scheduleDismissOnScreenControls(); 543 } 544 545 @Override 546 public void onCreate(Bundle instanceState) { 547 super.onCreate(instanceState); 548 549 Intent intent = getIntent(); 550 mFullScreenInNormalMode = intent.getBooleanExtra( 551 MediaStore.EXTRA_FULL_SCREEN, true); 552 mShowActionIcons = intent.getBooleanExtra( 553 MediaStore.EXTRA_SHOW_ACTION_ICONS, true); 554 555 mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 556 557 setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); 558 requestWindowFeature(Window.FEATURE_NO_TITLE); 559 setContentView(R.layout.viewimage); 560 561 mImageView = (ImageViewTouch) findViewById(R.id.image); 562 mImageView.setEnableTrackballScroll(true); 563 mCache = new BitmapCache(3); 564 mImageView.setRecycler(mCache); 565 566 makeGetter(); 567 568 mAnimationIndex = -1; 569 570 mSlideShowInAnimation = new Animation[] { 571 makeInAnimation(R.anim.transition_in), 572 makeInAnimation(R.anim.slide_in), 573 makeInAnimation(R.anim.slide_in_vertical), 574 }; 575 576 mSlideShowOutAnimation = new Animation[] { 577 makeOutAnimation(R.anim.transition_out), 578 makeOutAnimation(R.anim.slide_out), 579 makeOutAnimation(R.anim.slide_out_vertical), 580 }; 581 582 mSlideShowImageViews[0] = 583 (ImageViewTouchBase) findViewById(R.id.image1_slideShow); 584 mSlideShowImageViews[1] = 585 (ImageViewTouchBase) findViewById(R.id.image2_slideShow); 586 for (ImageViewTouchBase v : mSlideShowImageViews) { 587 v.setVisibility(View.INVISIBLE); 588 v.setRecycler(mCache); 589 } 590 591 mActionIconPanel = findViewById(R.id.action_icon_panel); 592 593 mParam = getIntent().getParcelableExtra(KEY_IMAGE_LIST); 594 595 boolean slideshow; 596 if (instanceState != null) { 597 mSavedUri = instanceState.getParcelable(STATE_URI); 598 slideshow = instanceState.getBoolean(STATE_SLIDESHOW, false); 599 mShowControls = instanceState.getBoolean(STATE_SHOW_CONTROLS, true); 600 } else { 601 mSavedUri = getIntent().getData(); 602 slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false); 603 } 604 605 // We only show action icons for URIs that we know we can share and 606 // delete. Although we get read permission (for the images) from 607 // applications like MMS, we cannot pass the permission to other 608 // activities due to the current framework design. 609 if (!MenuHelper.isWhiteListUri(mSavedUri)) { 610 mShowActionIcons = false; 611 } 612 613 if (mShowActionIcons) { 614 int[] pickIds = {R.id.attach, R.id.cancel}; 615 int[] normalIds = {R.id.setas, R.id.play, R.id.share, R.id.discard}; 616 int[] connectIds = isPickIntent() ? pickIds : normalIds; 617 for (int id : connectIds) { 618 View view = mActionIconPanel.findViewById(id); 619 view.setVisibility(View.VISIBLE); 620 view.setOnClickListener(this); 621 } 622 } 623 624 // Don't show the "delete" icon for SingleImageList. 625 if (ImageManager.isSingleImageMode(mSavedUri.toString())) { 626 mActionIconPanel.findViewById(R.id.discard) 627 .setVisibility(View.GONE); 628 } 629 630 if (slideshow) { 631 setMode(MODE_SLIDESHOW); 632 } else { 633 if (mFullScreenInNormalMode) { 634 getWindow().addFlags( 635 WindowManager.LayoutParams.FLAG_FULLSCREEN); 636 } 637 if (mShowActionIcons) { 638 mActionIconPanel.setVisibility(View.VISIBLE); 639 } 640 } 641 642 setupOnScreenControls(findViewById(R.id.rootLayout), mImageView); 643 } 644 645 private void updateActionIcons() { 646 if (isPickIntent()) return; 647 648 IImage image = mAllImages.getImageAt(mCurrentPosition); 649 View panel = mActionIconPanel; 650 if (image instanceof VideoObject) { 651 panel.findViewById(R.id.setas).setVisibility(View.GONE); 652 panel.findViewById(R.id.play).setVisibility(View.VISIBLE); 653 } else { 654 panel.findViewById(R.id.setas).setVisibility(View.VISIBLE); 655 panel.findViewById(R.id.play).setVisibility(View.GONE); 656 } 657 } 658 659 private Animation makeInAnimation(int id) { 660 Animation inAnimation = AnimationUtils.loadAnimation(this, id); 661 return inAnimation; 662 } 663 664 private Animation makeOutAnimation(int id) { 665 Animation outAnimation = AnimationUtils.loadAnimation(this, id); 666 return outAnimation; 667 } 668 669 private static int getPreferencesInteger( 670 SharedPreferences prefs, String key, int defaultValue) { 671 String value = prefs.getString(key, null); 672 try { 673 return value == null ? defaultValue : Integer.parseInt(value); 674 } catch (NumberFormatException ex) { 675 Log.e(TAG, "couldn't parse preference: " + value, ex); 676 return defaultValue; 677 } 678 } 679 680 void setMode(int mode) { 681 if (mMode == mode) { 682 return; 683 } 684 View slideshowPanel = findViewById(R.id.slideShowContainer); 685 View normalPanel = findViewById(R.id.abs); 686 687 Window win = getWindow(); 688 mMode = mode; 689 if (mode == MODE_SLIDESHOW) { 690 slideshowPanel.setVisibility(View.VISIBLE); 691 normalPanel.setVisibility(View.GONE); 692 693 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN 694 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 695 696 mImageView.clear(); 697 mActionIconPanel.setVisibility(View.GONE); 698 699 slideshowPanel.getRootView().requestLayout(); 700 701 // The preferences we want to read: 702 // mUseShuffleOrder 703 // mSlideShowLoop 704 // mAnimationIndex 705 // mSlideShowInterval 706 707 mUseShuffleOrder = mPrefs.getBoolean(PREF_SHUFFLE_SLIDESHOW, false); 708 mSlideShowLoop = mPrefs.getBoolean(PREF_SLIDESHOW_REPEAT, false); 709 mAnimationIndex = getPreferencesInteger( 710 mPrefs, "pref_gallery_slideshow_transition_key", 0); 711 mSlideShowInterval = getPreferencesInteger( 712 mPrefs, "pref_gallery_slideshow_interval_key", 3) * 1000; 713 } else { 714 slideshowPanel.setVisibility(View.GONE); 715 normalPanel.setVisibility(View.VISIBLE); 716 717 win.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 718 if (mFullScreenInNormalMode) { 719 win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 720 } else { 721 win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 722 } 723 724 if (mGetter != null) { 725 mGetter.cancelCurrent(); 726 } 727 728 if (mShowActionIcons) { 729 Animation animation = new AlphaAnimation(0F, 1F); 730 animation.setDuration(500); 731 mActionIconPanel.setAnimation(animation); 732 mActionIconPanel.setVisibility(View.VISIBLE); 733 } 734 735 ImageViewTouchBase dst = mImageView; 736 dst.mLastXTouchPos = -1; 737 dst.mLastYTouchPos = -1; 738 739 for (ImageViewTouchBase ivt : mSlideShowImageViews) { 740 ivt.clear(); 741 } 742 743 mShuffleOrder = null; 744 745 // mGetter null is a proxy for being paused 746 if (mGetter != null) { 747 setImage(mCurrentPosition, true); 748 } 749 } 750 } 751 752 private void generateShuffleOrder() { 753 if (mShuffleOrder == null 754 || mShuffleOrder.length != mAllImages.getCount()) { 755 mShuffleOrder = new int[mAllImages.getCount()]; 756 for (int i = 0, n = mShuffleOrder.length; i < n; i++) { 757 mShuffleOrder[i] = i; 758 } 759 } 760 761 for (int i = mShuffleOrder.length - 1; i >= 0; i--) { 762 int r = mRandom.nextInt(i + 1); 763 if (r != i) { 764 int tmp = mShuffleOrder[r]; 765 mShuffleOrder[r] = mShuffleOrder[i]; 766 mShuffleOrder[i] = tmp; 767 } 768 } 769 } 770 771 private void loadNextImage(final int requestedPos, final long delay, 772 final boolean firstCall) { 773 if (firstCall && mUseShuffleOrder) { 774 generateShuffleOrder(); 775 } 776 777 final long targetDisplayTime = System.currentTimeMillis() + delay; 778 779 ImageGetterCallback cb = new ImageGetterCallback() { 780 public void completed() { 781 } 782 783 public boolean wantsThumbnail(int pos, int offset) { 784 return true; 785 } 786 787 public boolean wantsFullImage(int pos, int offset) { 788 return false; 789 } 790 791 public int [] loadOrder() { 792 return sOrderSlideshow; 793 } 794 795 public int fullImageSizeToUse(int pos, int offset) { 796 return 480; // TODO compute this 797 } 798 799 public void imageLoaded(final int pos, final int offset, 800 final RotateBitmap bitmap, final boolean isThumb) { 801 long timeRemaining = Math.max(0, 802 targetDisplayTime - System.currentTimeMillis()); 803 mHandler.postDelayedGetterCallback(new Runnable() { 804 public void run() { 805 if (mMode == MODE_NORMAL) { 806 return; 807 } 808 809 ImageViewTouchBase oldView = 810 mSlideShowImageViews[mSlideShowImageCurrent]; 811 812 if (++mSlideShowImageCurrent 813 == mSlideShowImageViews.length) { 814 mSlideShowImageCurrent = 0; 815 } 816 817 ImageViewTouchBase newView = 818 mSlideShowImageViews[mSlideShowImageCurrent]; 819 newView.setVisibility(View.VISIBLE); 820 newView.setImageRotateBitmapResetBase(bitmap, true); 821 newView.bringToFront(); 822 823 int animation = 0; 824 825 if (mAnimationIndex == -1) { 826 int n = mRandom.nextInt( 827 mSlideShowInAnimation.length); 828 animation = n; 829 } else { 830 animation = mAnimationIndex; 831 } 832 833 Animation aIn = mSlideShowInAnimation[animation]; 834 newView.startAnimation(aIn); 835 newView.setVisibility(View.VISIBLE); 836 837 Animation aOut = mSlideShowOutAnimation[animation]; 838 oldView.setVisibility(View.INVISIBLE); 839 oldView.startAnimation(aOut); 840 841 mCurrentPosition = requestedPos; 842 843 if (mCurrentPosition == mLastSlideShowImage 844 && !firstCall) { 845 if (mSlideShowLoop) { 846 if (mUseShuffleOrder) { 847 generateShuffleOrder(); 848 } 849 } else { 850 setMode(MODE_NORMAL); 851 return; 852 } 853 } 854 855 loadNextImage( 856 (mCurrentPosition + 1) % mAllImages.getCount(), 857 mSlideShowInterval, false); 858 } 859 }, timeRemaining); 860 } 861 }; 862 // Could be null if we're stopping a slide show in the course of pausing 863 if (mGetter != null) { 864 int pos = requestedPos; 865 if (mShuffleOrder != null) { 866 pos = mShuffleOrder[pos]; 867 } 868 mGetter.setPosition(pos, cb, mAllImages, mHandler); 869 } 870 } 871 872 private void makeGetter() { 873 mGetter = new ImageGetter(getContentResolver()); 874 } 875 876 private IImageList buildImageListFromUri(Uri uri) { 877 String sortOrder = mPrefs.getString( 878 "pref_gallery_sort_key", "descending"); 879 int sort = sortOrder.equals("ascending") 880 ? ImageManager.SORT_ASCENDING 881 : ImageManager.SORT_DESCENDING; 882 return ImageManager.makeImageList(getContentResolver(), uri, sort); 883 } 884 885 private boolean init(Uri uri) { 886 if (uri == null) return false; 887 mAllImages = (mParam == null) 888 ? buildImageListFromUri(uri) 889 : ImageManager.makeImageList(getContentResolver(), mParam); 890 IImage image = mAllImages.getImageForUri(uri); 891 if (image == null) return false; 892 mCurrentPosition = mAllImages.getImageIndex(image); 893 mLastSlideShowImage = mCurrentPosition; 894 return true; 895 } 896 897 private Uri getCurrentUri() { 898 if (mAllImages.getCount() == 0) return null; 899 IImage image = mAllImages.getImageAt(mCurrentPosition); 900 return image.fullSizeImageUri(); 901 } 902 903 @Override 904 public void onSaveInstanceState(Bundle b) { 905 super.onSaveInstanceState(b); 906 b.putParcelable(STATE_URI, 907 mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri()); 908 b.putBoolean(STATE_SLIDESHOW, mMode == MODE_SLIDESHOW); 909 } 910 911 @Override 912 public void onStart() { 913 super.onStart(); 914 mPaused = false; 915 916 if (!init(mSavedUri)) { 917 Log.w(TAG, "init failed: " + mSavedUri); 918 finish(); 919 return; 920 } 921 922 // normally this will never be zero but if one "backs" into this 923 // activity after removing the sdcard it could be zero. in that 924 // case just "finish" since there's nothing useful that can happen. 925 int count = mAllImages.getCount(); 926 if (count == 0) { 927 finish(); 928 return; 929 } else if (count <= mCurrentPosition) { 930 mCurrentPosition = count - 1; 931 } 932 933 if (mGetter == null) { 934 makeGetter(); 935 } 936 937 if (mMode == MODE_SLIDESHOW) { 938 loadNextImage(mCurrentPosition, 0, true); 939 } else { // MODE_NORMAL 940 setImage(mCurrentPosition, mShowControls); 941 mShowControls = false; 942 } 943 } 944 945 @Override 946 public void onStop() { 947 super.onStop(); 948 mPaused = true; 949 950 // mGetter could be null if we call finish() and leave early in 951 // onStart(). 952 if (mGetter != null) { 953 mGetter.cancelCurrent(); 954 mGetter.stop(); 955 mGetter = null; 956 } 957 setMode(MODE_NORMAL); 958 959 // removing all callback in the message queue 960 mHandler.removeAllGetterCallbacks(); 961 962 if (mAllImages != null) { 963 mSavedUri = getCurrentUri(); 964 mAllImages.close(); 965 mAllImages = null; 966 } 967 968 hideOnScreenControls(); 969 mImageView.clear(); 970 mCache.clear(); 971 972 for (ImageViewTouchBase iv : mSlideShowImageViews) { 973 iv.clear(); 974 } 975 } 976 977 private void startShareMediaActivity(IImage image) { 978 boolean isVideo = image instanceof VideoObject; 979 Intent intent = new Intent(); 980 intent.setAction(Intent.ACTION_SEND); 981 intent.setType(image.getMimeType()); 982 intent.putExtra(Intent.EXTRA_STREAM, image.fullSizeImageUri()); 983 try { 984 startActivity(Intent.createChooser(intent, getText( 985 isVideo ? R.string.sendVideo : R.string.sendImage))); 986 } catch (android.content.ActivityNotFoundException ex) { 987 Toast.makeText(this, isVideo 988 ? R.string.no_way_to_share_image 989 : R.string.no_way_to_share_video, 990 Toast.LENGTH_SHORT).show(); 991 } 992 } 993 994 private void startPlayVideoActivity() { 995 IImage image = mAllImages.getImageAt(mCurrentPosition); 996 Intent intent = new Intent( 997 Intent.ACTION_VIEW, image.fullSizeImageUri()); 998 try { 999 startActivity(intent); 1000 } catch (android.content.ActivityNotFoundException ex) { 1001 Log.e(TAG, "Couldn't view video " + image.fullSizeImageUri(), ex); 1002 } 1003 } 1004 1005 public void onClick(View v) { 1006 switch (v.getId()) { 1007 case R.id.discard: 1008 MenuHelper.deletePhoto(this, mDeletePhotoRunnable); 1009 break; 1010 case R.id.play: 1011 startPlayVideoActivity(); 1012 break; 1013 case R.id.share: { 1014 IImage image = mAllImages.getImageAt(mCurrentPosition); 1015 if (!MenuHelper.isWhiteListUri(image.fullSizeImageUri())) { 1016 return; 1017 } 1018 startShareMediaActivity(image); 1019 break; 1020 } 1021 case R.id.setas: { 1022 IImage image = mAllImages.getImageAt(mCurrentPosition); 1023 Intent intent = Util.createSetAsIntent(image); 1024 try { 1025 startActivity(Intent.createChooser( 1026 intent, getText(R.string.setImage))); 1027 } catch (android.content.ActivityNotFoundException ex) { 1028 Toast.makeText(this, R.string.no_way_to_share_video, 1029 Toast.LENGTH_SHORT).show(); 1030 } 1031 break; 1032 } 1033 case R.id.next_image: 1034 moveNextOrPrevious(1); 1035 break; 1036 case R.id.prev_image: 1037 moveNextOrPrevious(-1); 1038 break; 1039 } 1040 } 1041 1042 private void moveNextOrPrevious(int delta) { 1043 int nextImagePos = mCurrentPosition + delta; 1044 if ((0 <= nextImagePos) && (nextImagePos < mAllImages.getCount())) { 1045 setImage(nextImagePos, true); 1046 showOnScreenControls(); 1047 } 1048 } 1049 1050 @Override 1051 protected void onActivityResult(int requestCode, int resultCode, 1052 Intent data) { 1053 switch (requestCode) { 1054 case MenuHelper.RESULT_COMMON_MENU_CROP: 1055 if (resultCode == RESULT_OK) { 1056 // The CropImage activity passes back the Uri of the 1057 // cropped image as the Action rather than the Data. 1058 mSavedUri = Uri.parse(data.getAction()); 1059 1060 // if onStart() runs before, then set the returned 1061 // image as currentImage. 1062 if (mAllImages != null) { 1063 IImage image = mAllImages.getImageForUri(mSavedUri); 1064 // image could be null if SD card is removed. 1065 if (image == null) { 1066 finish(); 1067 } else { 1068 mCurrentPosition = mAllImages.getImageIndex(image); 1069 setImage(mCurrentPosition, false); 1070 } 1071 } 1072 } 1073 break; 1074 } 1075 } 1076} 1077 1078class ImageViewTouch extends ImageViewTouchBase { 1079 private final ViewImage mViewImage; 1080 private boolean mEnableTrackballScroll; 1081 1082 public ImageViewTouch(Context context) { 1083 super(context); 1084 mViewImage = (ViewImage) context; 1085 } 1086 1087 public ImageViewTouch(Context context, AttributeSet attrs) { 1088 super(context, attrs); 1089 mViewImage = (ViewImage) context; 1090 } 1091 1092 public void setEnableTrackballScroll(boolean enable) { 1093 mEnableTrackballScroll = enable; 1094 } 1095 1096 protected void postTranslateCenter(float dx, float dy) { 1097 super.postTranslate(dx, dy); 1098 center(true, true); 1099 } 1100 1101 private static final float PAN_RATE = 20; 1102 1103 // This is the time we allow the dpad to change the image position again. 1104 private long mNextChangePositionTime; 1105 1106 @Override 1107 public boolean onKeyDown(int keyCode, KeyEvent event) { 1108 if (mViewImage.mPaused) return false; 1109 1110 // Don't respond to arrow keys if trackball scrolling is not enabled 1111 if (!mEnableTrackballScroll) { 1112 if ((keyCode >= KeyEvent.KEYCODE_DPAD_UP) 1113 && (keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT)) { 1114 return super.onKeyDown(keyCode, event); 1115 } 1116 } 1117 1118 int current = mViewImage.mCurrentPosition; 1119 1120 int nextImagePos = -2; // default no next image 1121 try { 1122 switch (keyCode) { 1123 case KeyEvent.KEYCODE_DPAD_CENTER: { 1124 if (mViewImage.isPickIntent()) { 1125 IImage img = mViewImage.mAllImages 1126 .getImageAt(mViewImage.mCurrentPosition); 1127 mViewImage.setResult(ViewImage.RESULT_OK, 1128 new Intent().setData(img.fullSizeImageUri())); 1129 mViewImage.finish(); 1130 } 1131 break; 1132 } 1133 case KeyEvent.KEYCODE_DPAD_LEFT: { 1134 if (getScale() <= 1F && event.getEventTime() 1135 >= mNextChangePositionTime) { 1136 nextImagePos = current - 1; 1137 mNextChangePositionTime = event.getEventTime() + 500; 1138 } else { 1139 panBy(PAN_RATE, 0); 1140 center(true, false); 1141 } 1142 return true; 1143 } 1144 case KeyEvent.KEYCODE_DPAD_RIGHT: { 1145 if (getScale() <= 1F && event.getEventTime() 1146 >= mNextChangePositionTime) { 1147 nextImagePos = current + 1; 1148 mNextChangePositionTime = event.getEventTime() + 500; 1149 } else { 1150 panBy(-PAN_RATE, 0); 1151 center(true, false); 1152 } 1153 return true; 1154 } 1155 case KeyEvent.KEYCODE_DPAD_UP: { 1156 panBy(0, PAN_RATE); 1157 center(false, true); 1158 return true; 1159 } 1160 case KeyEvent.KEYCODE_DPAD_DOWN: { 1161 panBy(0, -PAN_RATE); 1162 center(false, true); 1163 return true; 1164 } 1165 case KeyEvent.KEYCODE_DEL: 1166 MenuHelper.deletePhoto( 1167 mViewImage, mViewImage.mDeletePhotoRunnable); 1168 break; 1169 } 1170 } finally { 1171 if (nextImagePos >= 0 1172 && nextImagePos < mViewImage.mAllImages.getCount()) { 1173 synchronized (mViewImage) { 1174 mViewImage.setMode(ViewImage.MODE_NORMAL); 1175 mViewImage.setImage(nextImagePos, true); 1176 } 1177 } else if (nextImagePos != -2) { 1178 center(true, true); 1179 } 1180 } 1181 1182 return super.onKeyDown(keyCode, event); 1183 } 1184} 1185 1186// This is a cache for Bitmap displayed in ViewImage (normal mode, thumb only). 1187class BitmapCache implements ImageViewTouchBase.Recycler { 1188 public static class Entry { 1189 int mPos; 1190 Bitmap mBitmap; 1191 public Entry() { 1192 clear(); 1193 } 1194 public void clear() { 1195 mPos = -1; 1196 mBitmap = null; 1197 } 1198 } 1199 1200 private final Entry[] mCache; 1201 1202 public BitmapCache(int size) { 1203 mCache = new Entry[size]; 1204 for (int i = 0; i < mCache.length; i++) { 1205 mCache[i] = new Entry(); 1206 } 1207 } 1208 1209 // Given the position, find the associated entry. Returns null if there is 1210 // no such entry. 1211 private Entry findEntry(int pos) { 1212 for (Entry e : mCache) { 1213 if (pos == e.mPos) { 1214 return e; 1215 } 1216 } 1217 return null; 1218 } 1219 1220 // Returns the thumb bitmap if we have it, otherwise return null. 1221 public synchronized Bitmap getBitmap(int pos) { 1222 Entry e = findEntry(pos); 1223 if (e != null) { 1224 return e.mBitmap; 1225 } 1226 return null; 1227 } 1228 1229 public synchronized void put(int pos, Bitmap bitmap) { 1230 // First see if we already have this entry. 1231 if (findEntry(pos) != null) { 1232 return; 1233 } 1234 1235 // Find the best entry we should replace. 1236 // See if there is any empty entry. 1237 // Otherwise assuming sequential access, kick out the entry with the 1238 // greatest distance. 1239 Entry best = null; 1240 int maxDist = -1; 1241 for (Entry e : mCache) { 1242 if (e.mPos == -1) { 1243 best = e; 1244 break; 1245 } else { 1246 int dist = Math.abs(pos - e.mPos); 1247 if (dist > maxDist) { 1248 maxDist = dist; 1249 best = e; 1250 } 1251 } 1252 } 1253 1254 // Recycle the image being kicked out. 1255 // This only works because our current usage is sequential, so we 1256 // do not happen to recycle the image being displayed. 1257 if (best.mBitmap != null) { 1258 best.mBitmap.recycle(); 1259 } 1260 1261 best.mPos = pos; 1262 best.mBitmap = bitmap; 1263 } 1264 1265 // Recycle all bitmaps in the cache and clear the cache. 1266 public synchronized void clear() { 1267 for (Entry e : mCache) { 1268 if (e.mBitmap != null) { 1269 e.mBitmap.recycle(); 1270 } 1271 e.clear(); 1272 } 1273 } 1274 1275 // Returns whether the bitmap is in the cache. 1276 public synchronized boolean hasBitmap(int pos) { 1277 Entry e = findEntry(pos); 1278 return (e != null); 1279 } 1280 1281 // Recycle the bitmap if it's not in the cache. 1282 // The input must be non-null. 1283 public synchronized void recycle(Bitmap b) { 1284 for (Entry e : mCache) { 1285 if (e.mPos != -1) { 1286 if (e.mBitmap == b) { 1287 return; 1288 } 1289 } 1290 } 1291 b.recycle(); 1292 } 1293} 1294