1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.videoeditor; 18 19 import android.app.Activity; 20 import android.content.Intent; 21 import android.graphics.Bitmap; 22 import android.graphics.BitmapFactory; 23 import android.graphics.Rect; 24 import android.graphics.RectF; 25 import android.os.AsyncTask; 26 import android.os.Bundle; 27 import android.util.Log; 28 import android.view.GestureDetector; 29 import android.view.MotionEvent; 30 import android.view.ScaleGestureDetector; 31 import android.view.View; 32 import android.view.ScaleGestureDetector.OnScaleGestureListener; 33 import android.widget.FrameLayout; 34 import android.widget.RadioGroup; 35 import android.widget.Toast; 36 37 import com.android.videoeditor.widgets.ImageViewTouchBase; 38 39 /** 40 * Activity for setting the begin and end Ken Burns viewing rectangles 41 */ 42 public class KenBurnsActivity extends Activity { 43 // Logging 44 private static final String TAG = "KenBurnsActivity"; 45 46 // State keys 47 private static final String STATE_WHICH_RECTANGLE_ID = "which"; 48 private static final String STATE_START_RECTANGLE = "start"; 49 private static final String STATE_END_RECTANGLE = "end"; 50 51 // Intent extras 52 public static final String PARAM_WIDTH = "width"; 53 public static final String PARAM_HEIGHT = "height"; 54 public static final String PARAM_FILENAME = "filename"; 55 public static final String PARAM_MEDIA_ITEM_ID = "media_item_id"; 56 public static final String PARAM_START_RECT = "start_rect"; 57 public static final String PARAM_END_RECT = "end_rect"; 58 59 private static final int MAX_HW_BITMAP_WIDTH = 2048; 60 private static final int MAX_HW_BITMAP_HEIGHT = 2048; 61 private static final int MAX_WIDTH = 1296; 62 private static final int MAX_HEIGHT = 720; 63 private static final int MAX_PAN = 3; 64 65 // Instance variables 66 private final Rect mStartRect = new Rect(0, 0, 0, 0); 67 private final Rect mEndRect = new Rect(0, 0, 0, 0); 68 private final RectF mMatrixRect = new RectF(0, 0, 0, 0); 69 private RadioGroup mRadioGroup; 70 private ImageViewTouchBase mImageView; 71 private View mDoneButton; 72 private GestureDetector mGestureDetector; 73 private ScaleGestureDetector mScaleGestureDetector; 74 private boolean mPaused = true; 75 private int mMediaItemWidth, mMediaItemHeight; 76 private float mImageViewScale; 77 private int mImageSubsample; 78 private Bitmap mBitmap; 79 80 /** 81 * The simple gestures listener 82 */ 83 private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { 84 @Override 85 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 86 if (mImageView.getScale() > 1F) { 87 mImageView.postTranslateCenter(-distanceX, -distanceY); 88 saveBitmapRectangle(); 89 } 90 91 return true; 92 } 93 94 @Override 95 public boolean onSingleTapUp(MotionEvent e) { 96 return true; 97 } 98 99 @Override 100 public boolean onDoubleTap(MotionEvent e) { 101 // Switch between the original scale and 3x scale. 102 if (mImageView.getScale() > 2F) { 103 mImageView.zoomTo(1F); 104 } else { 105 mImageView.zoomTo(3F, e.getX(), e.getY()); 106 } 107 108 saveBitmapRectangle(); 109 return true; 110 } 111 } 112 113 /** 114 * Scale gesture listener 115 */ 116 private class MyScaleGestureListener implements OnScaleGestureListener { 117 @Override 118 public boolean onScaleBegin(ScaleGestureDetector detector) { 119 return true; 120 } 121 122 @Override 123 public boolean onScale(ScaleGestureDetector detector) { 124 final float relativeScaleFactor = detector.getScaleFactor(); 125 final float newAbsoluteScale = relativeScaleFactor * mImageView.getScale(); 126 if (newAbsoluteScale < 1.0F) { 127 return false; 128 } 129 130 mImageView.zoomTo(newAbsoluteScale, detector.getFocusX(), detector.getFocusY()); 131 return true; 132 } 133 134 @Override 135 public void onScaleEnd(ScaleGestureDetector detector) { 136 saveBitmapRectangle(); 137 } 138 } 139 140 /** 141 * Image loader class 142 */ 143 private class ImageLoaderAsyncTask extends AsyncTask<Void, Void, Bitmap> { 144 // Instance variables 145 private final String mFilename; 146 147 /** 148 * Constructor 149 * 150 * @param filename The filename 151 */ 152 public ImageLoaderAsyncTask(String filename) { 153 mFilename = filename; 154 showProgress(true); 155 } 156 157 @Override 158 protected Bitmap doInBackground(Void... zzz) { 159 if (mPaused) { 160 return null; 161 } 162 163 // Wait for the layout to complete 164 while (mImageView.getWidth() <= 0) { 165 try { 166 Thread.sleep(30); 167 } catch (InterruptedException ex) { 168 } 169 } 170 171 if (mBitmap != null) { 172 return mBitmap; 173 } else { 174 final BitmapFactory.Options options = new BitmapFactory.Options(); 175 options.inSampleSize = mImageSubsample; 176 return BitmapFactory.decodeFile(mFilename, options); 177 } 178 } 179 180 @Override 181 protected void onPostExecute(Bitmap bitmap) { 182 if (bitmap == null) { 183 if (!mPaused) { 184 finish(); 185 } 186 return; 187 } 188 189 if (!mPaused) { 190 showProgress(false); 191 mRadioGroup.setEnabled(true); 192 mImageView.setImageBitmapResetBase(bitmap, true); 193 mBitmap = bitmap; 194 if (Log.isLoggable(TAG, Log.DEBUG)) { 195 Log.d(TAG, "Bitmap size: " + bitmap.getWidth() + "x" + bitmap.getHeight() 196 + ", bytes: " + (bitmap.getRowBytes() * bitmap.getHeight())); 197 } 198 199 showBitmapRectangle(); 200 } else { 201 bitmap.recycle(); 202 } 203 } 204 } 205 206 @Override 207 public void onCreate(Bundle state) { 208 super.onCreate(state); 209 setContentView(R.layout.ken_burns_layout); 210 setFinishOnTouchOutside(true); 211 212 mMediaItemWidth = getIntent().getIntExtra(PARAM_WIDTH, 0); 213 mMediaItemHeight = getIntent().getIntExtra(PARAM_HEIGHT, 0); 214 if (Log.isLoggable(TAG, Log.DEBUG)) { 215 Log.d(TAG, "Media item size: " + mMediaItemWidth + "x" + mMediaItemHeight); 216 } 217 218 // Setup the image view 219 mImageView = (ImageViewTouchBase)findViewById(R.id.ken_burns_image); 220 221 // Set the width and height of the image view 222 final FrameLayout.LayoutParams lp = 223 (FrameLayout.LayoutParams)mImageView.getLayoutParams(); 224 if (mMediaItemWidth >= mMediaItemHeight) { 225 lp.width = Math.min(mMediaItemWidth, MAX_WIDTH) / MAX_PAN; 226 // Compute the height by preserving the aspect ratio 227 lp.height = (lp.width * mMediaItemHeight) / mMediaItemWidth; 228 mImageSubsample = mMediaItemWidth / (lp.width * MAX_PAN); 229 } else { 230 lp.height = Math.min(mMediaItemHeight, MAX_HEIGHT) / MAX_PAN; 231 // Compute the width by preserving the aspect ratio 232 lp.width = (lp.height * mMediaItemWidth) / mMediaItemHeight; 233 mImageSubsample = mMediaItemHeight / (lp.height * MAX_PAN); 234 } 235 236 // Ensure that the size of the bitmap will not exceed the size supported 237 // by HW vendors 238 while ((mMediaItemWidth / mImageSubsample > MAX_HW_BITMAP_WIDTH) || 239 (mMediaItemHeight / mImageSubsample > MAX_HW_BITMAP_HEIGHT)) { 240 mImageSubsample++; 241 } 242 243 if (Log.isLoggable(TAG, Log.DEBUG)) { 244 Log.d(TAG, "View size: " + lp.width + "x" + lp.height 245 + ", subsample: " + mImageSubsample); 246 } 247 248 // If the image is too small the image view may be too small to pinch 249 if (lp.width < 120 || lp.height < 120) { 250 if (Log.isLoggable(TAG, Log.DEBUG)) { 251 Log.d(TAG, "Image is too small: " + lp.width + "x" + lp.height); 252 } 253 254 Toast.makeText(this, getString(R.string.pan_zoom_small_image_error), 255 Toast.LENGTH_LONG).show(); 256 finish(); 257 return; 258 } 259 260 mImageView.setLayoutParams(lp); 261 mImageViewScale = ((float)lp.width) / ((float)mMediaItemWidth); 262 263 mGestureDetector = new GestureDetector(this, new MyGestureListener()); 264 mScaleGestureDetector = new ScaleGestureDetector(this, new MyScaleGestureListener()); 265 266 mRadioGroup = (RadioGroup)findViewById(R.id.which_rectangle); 267 if (state != null) { 268 mRadioGroup.check(state.getInt(STATE_WHICH_RECTANGLE_ID)); 269 mStartRect.set((Rect)state.getParcelable(STATE_START_RECTANGLE)); 270 mEndRect.set((Rect)state.getParcelable(STATE_END_RECTANGLE)); 271 } else { 272 mRadioGroup.check(R.id.start_rectangle); 273 final Rect startRect = (Rect)getIntent().getParcelableExtra(PARAM_START_RECT); 274 if (startRect != null) { 275 mStartRect.set(startRect); 276 } else { 277 mStartRect.set(0, 0, mMediaItemWidth, mMediaItemHeight); 278 } 279 280 final Rect endRect = (Rect)getIntent().getParcelableExtra(PARAM_END_RECT); 281 if (endRect != null) { 282 mEndRect.set(endRect); 283 } else { 284 mEndRect.set(0, 0, mMediaItemWidth, mMediaItemHeight); 285 } 286 } 287 288 mDoneButton = findViewById(R.id.done); 289 enableDoneButton(); 290 291 // Disable the ratio buttons until we load the image 292 mRadioGroup.setEnabled(false); 293 294 mRadioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { 295 @Override 296 public void onCheckedChanged(RadioGroup group, int checkedId) { 297 switch (checkedId) { 298 case R.id.start_rectangle: { 299 showBitmapRectangle(); 300 break; 301 } 302 303 case R.id.end_rectangle: { 304 showBitmapRectangle(); 305 break; 306 } 307 308 case R.id.done: { 309 final Intent extra = new Intent(); 310 extra.putExtra(PARAM_MEDIA_ITEM_ID, 311 getIntent().getStringExtra(PARAM_MEDIA_ITEM_ID)); 312 extra.putExtra(PARAM_START_RECT, mStartRect); 313 extra.putExtra(PARAM_END_RECT, mEndRect); 314 setResult(RESULT_OK, extra); 315 finish(); 316 break; 317 } 318 319 default: { 320 break; 321 } 322 } 323 } 324 }); 325 326 mBitmap = (Bitmap) getLastNonConfigurationInstance(); 327 328 mImageView.setEventListener(new ImageViewTouchBase.ImageTouchEventListener() { 329 @Override 330 public boolean onImageTouchEvent(MotionEvent ev) { 331 if (null != mScaleGestureDetector) { 332 mScaleGestureDetector.onTouchEvent(ev); 333 if (mScaleGestureDetector.isInProgress()) { 334 return true; 335 } 336 } 337 338 mGestureDetector.onTouchEvent(ev); 339 return true; 340 } 341 }); 342 } 343 344 @Override 345 protected void onResume() { 346 super.onResume(); 347 348 mPaused = false; 349 // Load the image 350 new ImageLoaderAsyncTask(getIntent().getStringExtra(PARAM_FILENAME)).execute(); 351 } 352 353 @Override 354 protected void onPause() { 355 super.onPause(); 356 357 mPaused = true; 358 } 359 360 @Override 361 protected void onDestroy() { 362 super.onDestroy(); 363 if (!isChangingConfigurations()) { 364 if (mBitmap != null) { 365 mBitmap.recycle(); 366 mBitmap = null; 367 } 368 369 System.gc(); 370 } 371 } 372 373 @Override 374 public Object onRetainNonConfigurationInstance() { 375 return mBitmap; 376 } 377 378 @Override 379 public void onSaveInstanceState(Bundle outState) { 380 super.onSaveInstanceState(outState); 381 final RadioGroup radioGroup = (RadioGroup)findViewById(R.id.which_rectangle); 382 383 outState.putInt(STATE_WHICH_RECTANGLE_ID, radioGroup.getCheckedRadioButtonId()); 384 outState.putParcelable(STATE_START_RECTANGLE, mStartRect); 385 outState.putParcelable(STATE_END_RECTANGLE, mEndRect); 386 } 387 388 public void onClickHandler(View target) { 389 switch (target.getId()) { 390 case R.id.done: { 391 final Intent extra = new Intent(); 392 extra.putExtra(PARAM_MEDIA_ITEM_ID, 393 getIntent().getStringExtra(PARAM_MEDIA_ITEM_ID)); 394 extra.putExtra(PARAM_START_RECT, mStartRect); 395 extra.putExtra(PARAM_END_RECT, mEndRect); 396 setResult(RESULT_OK, extra); 397 finish(); 398 break; 399 } 400 401 default: { 402 break; 403 } 404 } 405 } 406 407 /** 408 * Show/hide the progress bar 409 * 410 * @param show true to show the progress 411 */ 412 private void showProgress(boolean show) { 413 if (show) { 414 findViewById(R.id.image_loading).setVisibility(View.VISIBLE); 415 } else { 416 findViewById(R.id.image_loading).setVisibility(View.GONE); 417 } 418 } 419 420 /** 421 * Enable the "Done" button if both rectangles are set 422 */ 423 private void enableDoneButton() { 424 mDoneButton.setEnabled(!mStartRect.isEmpty() && !mEndRect.isEmpty()); 425 } 426 427 /** 428 * Show the bitmap rectangle 429 */ 430 private void showBitmapRectangle() { 431 final int checkedRect = mRadioGroup.getCheckedRadioButtonId(); 432 switch (checkedRect) { 433 case R.id.start_rectangle: { 434 if (!mStartRect.isEmpty()) { 435 mImageView.reset(); 436 final float scale = ((float)mMediaItemWidth) 437 / ((float)(mStartRect.right - mStartRect.left)); 438 if (Log.isLoggable(TAG, Log.DEBUG)) { 439 Log.d(TAG, "showBitmapRectangle START: " + scale + " " 440 + mStartRect.left + ", " + mStartRect.top + ", " 441 + mStartRect.right + ", " + mStartRect.bottom); 442 } 443 if (scale > 1F) { 444 mImageView.zoomToOffset(scale, mStartRect.left * scale * mImageViewScale, 445 mStartRect.top * scale * mImageViewScale); 446 } 447 } 448 break; 449 } 450 451 case R.id.end_rectangle: { 452 if (!mEndRect.isEmpty()) { 453 mImageView.reset(); 454 final float scale = ((float)mMediaItemWidth) 455 / ((float)(mEndRect.right - mEndRect.left)); 456 if (Log.isLoggable(TAG, Log.DEBUG)) { 457 Log.d(TAG, "showBitmapRectangle END: " + scale + " " 458 + mEndRect.left + ", " + mEndRect.top + ", " 459 + mEndRect.right + ", " + mEndRect.bottom); 460 } 461 if (scale > 1F) { 462 mImageView.zoomToOffset(scale, mEndRect.left * scale * mImageViewScale, 463 mEndRect.top * scale * mImageViewScale); 464 } 465 } 466 break; 467 } 468 469 default: { 470 break; 471 } 472 } 473 } 474 475 /** 476 * Show the bitmap rectangle 477 */ 478 private void saveBitmapRectangle() { 479 final int checkedRect = mRadioGroup.getCheckedRadioButtonId(); 480 final FrameLayout.LayoutParams lp = 481 (FrameLayout.LayoutParams)mImageView.getLayoutParams(); 482 switch (checkedRect) { 483 case R.id.start_rectangle: { 484 mMatrixRect.set(0, 0, lp.width, lp.height); 485 486 mImageView.mapRect(mMatrixRect); 487 final float scale = mImageView.getScale(); 488 489 if (Log.isLoggable(TAG, Log.DEBUG)) { 490 Log.d(TAG, "START RAW: " + scale + ", rect: " + mMatrixRect.left 491 + ", " + mMatrixRect.top + ", " + mMatrixRect.right 492 + ", " + mMatrixRect.bottom); 493 } 494 495 final int left = (int)((-mMatrixRect.left/scale) / mImageViewScale); 496 final int top = (int)((-mMatrixRect.top/scale) / mImageViewScale); 497 final int right = (int)(((-mMatrixRect.left + lp.width)/scale) / mImageViewScale); 498 final int bottom = (int)(((-mMatrixRect.top + lp.height)/scale) / mImageViewScale); 499 500 mStartRect.set(left, top, right, bottom); 501 if (Log.isLoggable(TAG, Log.DEBUG)) { 502 Log.d(TAG, "START: " + mStartRect.left + ", " + mStartRect.top + ", " 503 + mStartRect.right + ", " + mStartRect.bottom); 504 } 505 506 enableDoneButton(); 507 break; 508 } 509 510 case R.id.end_rectangle: { 511 mMatrixRect.set(0, 0, lp.width, lp.height); 512 513 mImageView.mapRect(mMatrixRect); 514 final float scale = mImageView.getScale(); 515 516 if (Log.isLoggable(TAG, Log.DEBUG)) { 517 Log.d(TAG, "END RAW: " + scale + ", rect: " + mMatrixRect.left 518 + ", " + mMatrixRect.top + ", " + mMatrixRect.right 519 + ", " + mMatrixRect.bottom); 520 } 521 522 final int left = (int)((-mMatrixRect.left/scale) / mImageViewScale); 523 final int top = (int)((-mMatrixRect.top/scale) / mImageViewScale); 524 final int right = (int)(((-mMatrixRect.left + lp.width)/scale) / mImageViewScale); 525 final int bottom = (int)(((-mMatrixRect.top + lp.height)/scale) / mImageViewScale); 526 527 mEndRect.set(left, top, right, bottom); 528 if (Log.isLoggable(TAG, Log.DEBUG)) { 529 Log.d(TAG, "END: " + mEndRect.left + ", " + mEndRect.top + ", " 530 + mEndRect.right + ", " + mEndRect.bottom); 531 } 532 533 enableDoneButton(); 534 break; 535 } 536 537 default: { 538 break; 539 } 540 } 541 } 542 } 543