• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)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
onSingleTapUp(MotionEvent e)95         public boolean onSingleTapUp(MotionEvent e) {
96             return true;
97         }
98 
99         @Override
onDoubleTap(MotionEvent e)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
onScaleBegin(ScaleGestureDetector detector)118         public boolean onScaleBegin(ScaleGestureDetector detector) {
119             return true;
120         }
121 
122         @Override
onScale(ScaleGestureDetector detector)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
onScaleEnd(ScaleGestureDetector detector)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          */
ImageLoaderAsyncTask(String filename)152         public ImageLoaderAsyncTask(String filename) {
153             mFilename = filename;
154             showProgress(true);
155         }
156 
157         @Override
doInBackground(Void... zzz)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
onPostExecute(Bitmap bitmap)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             }
201         }
202     }
203 
204     @Override
onCreate(Bundle state)205     public void onCreate(Bundle state) {
206         super.onCreate(state);
207         setContentView(R.layout.ken_burns_layout);
208         setFinishOnTouchOutside(true);
209 
210         mMediaItemWidth = getIntent().getIntExtra(PARAM_WIDTH, 0);
211         mMediaItemHeight = getIntent().getIntExtra(PARAM_HEIGHT, 0);
212         if (Log.isLoggable(TAG, Log.DEBUG)) {
213             Log.d(TAG, "Media item size: " + mMediaItemWidth + "x" + mMediaItemHeight);
214         }
215 
216         // Setup the image view
217         mImageView = (ImageViewTouchBase)findViewById(R.id.ken_burns_image);
218 
219         // Set the width and height of the image view
220         final FrameLayout.LayoutParams lp =
221             (FrameLayout.LayoutParams)mImageView.getLayoutParams();
222         if (mMediaItemWidth >= mMediaItemHeight) {
223             lp.width = Math.min(mMediaItemWidth, MAX_WIDTH) / MAX_PAN;
224             // Compute the height by preserving the aspect ratio
225             lp.height = (lp.width * mMediaItemHeight) / mMediaItemWidth;
226             mImageSubsample = mMediaItemWidth / (lp.width * MAX_PAN);
227         } else {
228             lp.height = Math.min(mMediaItemHeight, MAX_HEIGHT) / MAX_PAN;
229             // Compute the width by preserving the aspect ratio
230             lp.width = (lp.height * mMediaItemWidth) / mMediaItemHeight;
231             mImageSubsample = mMediaItemHeight / (lp.height * MAX_PAN);
232         }
233 
234         // Ensure that the size of the bitmap will not exceed the size supported
235         // by HW vendors
236         while ((mMediaItemWidth / mImageSubsample > MAX_HW_BITMAP_WIDTH) ||
237                 (mMediaItemHeight / mImageSubsample > MAX_HW_BITMAP_HEIGHT)) {
238             mImageSubsample++;
239         }
240 
241         if (Log.isLoggable(TAG, Log.DEBUG)) {
242             Log.d(TAG, "View size: " + lp.width + "x" + lp.height
243                     + ", subsample: " + mImageSubsample);
244         }
245 
246         // If the image is too small the image view may be too small to pinch
247         if (lp.width < 120 || lp.height < 120) {
248             if (Log.isLoggable(TAG, Log.DEBUG)) {
249                 Log.d(TAG, "Image is too small: " + lp.width + "x" + lp.height);
250             }
251 
252             Toast.makeText(this, getString(R.string.pan_zoom_small_image_error),
253                     Toast.LENGTH_LONG).show();
254             finish();
255             return;
256         }
257 
258         mImageView.setLayoutParams(lp);
259         mImageViewScale = ((float)lp.width) / ((float)mMediaItemWidth);
260 
261         mGestureDetector = new GestureDetector(this, new MyGestureListener());
262         mScaleGestureDetector = new ScaleGestureDetector(this, new MyScaleGestureListener());
263 
264         mRadioGroup = (RadioGroup)findViewById(R.id.which_rectangle);
265         if (state != null) {
266             mRadioGroup.check(state.getInt(STATE_WHICH_RECTANGLE_ID));
267             mStartRect.set((Rect)state.getParcelable(STATE_START_RECTANGLE));
268             mEndRect.set((Rect)state.getParcelable(STATE_END_RECTANGLE));
269         } else {
270             mRadioGroup.check(R.id.start_rectangle);
271             final Rect startRect = (Rect)getIntent().getParcelableExtra(PARAM_START_RECT);
272             if (startRect != null) {
273                 mStartRect.set(startRect);
274             } else {
275                 mStartRect.set(0, 0, mMediaItemWidth, mMediaItemHeight);
276             }
277 
278             final Rect endRect = (Rect)getIntent().getParcelableExtra(PARAM_END_RECT);
279             if (endRect != null) {
280                 mEndRect.set(endRect);
281             } else {
282                 mEndRect.set(0, 0, mMediaItemWidth, mMediaItemHeight);
283             }
284         }
285 
286         mDoneButton = findViewById(R.id.done);
287         enableDoneButton();
288 
289         // Disable the ratio buttons until we load the image
290         mRadioGroup.setEnabled(false);
291 
292         mRadioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
293             @Override
294             public void onCheckedChanged(RadioGroup group, int checkedId) {
295                 switch (checkedId) {
296                     case R.id.start_rectangle: {
297                         showBitmapRectangle();
298                         break;
299                     }
300 
301                     case R.id.end_rectangle: {
302                         showBitmapRectangle();
303                         break;
304                     }
305 
306                     case R.id.done: {
307                         final Intent extra = new Intent();
308                         extra.putExtra(PARAM_MEDIA_ITEM_ID,
309                                 getIntent().getStringExtra(PARAM_MEDIA_ITEM_ID));
310                         extra.putExtra(PARAM_START_RECT, mStartRect);
311                         extra.putExtra(PARAM_END_RECT, mEndRect);
312                         setResult(RESULT_OK, extra);
313                         finish();
314                         break;
315                     }
316 
317                     default: {
318                         break;
319                     }
320                 }
321             }
322         });
323 
324         mBitmap = (Bitmap) getLastNonConfigurationInstance();
325 
326         mImageView.setEventListener(new ImageViewTouchBase.ImageTouchEventListener() {
327             @Override
328             public boolean onImageTouchEvent(MotionEvent ev) {
329                 mScaleGestureDetector.onTouchEvent(ev);
330                 mGestureDetector.onTouchEvent(ev);
331                 return true;
332             }
333         });
334     }
335 
336     @Override
onResume()337     protected void onResume() {
338         super.onResume();
339 
340         mPaused = false;
341         // Load the image
342         new ImageLoaderAsyncTask(getIntent().getStringExtra(PARAM_FILENAME)).execute();
343     }
344 
345     @Override
onPause()346     protected void onPause() {
347         super.onPause();
348 
349         mPaused = true;
350     }
351 
352     @Override
onDestroy()353     protected void onDestroy() {
354         super.onDestroy();
355         if (!isChangingConfigurations()) {
356             if (mBitmap != null) {
357                 mBitmap.recycle();
358                 mBitmap = null;
359             }
360 
361             System.gc();
362         }
363     }
364 
365     @Override
onRetainNonConfigurationInstance()366     public Object onRetainNonConfigurationInstance() {
367         return mBitmap;
368     }
369 
370     @Override
onSaveInstanceState(Bundle outState)371     public void onSaveInstanceState(Bundle outState) {
372         super.onSaveInstanceState(outState);
373         final RadioGroup radioGroup = (RadioGroup)findViewById(R.id.which_rectangle);
374 
375         outState.putInt(STATE_WHICH_RECTANGLE_ID, radioGroup.getCheckedRadioButtonId());
376         outState.putParcelable(STATE_START_RECTANGLE, mStartRect);
377         outState.putParcelable(STATE_END_RECTANGLE, mEndRect);
378     }
379 
onClickHandler(View target)380     public void onClickHandler(View target) {
381         switch (target.getId()) {
382             case R.id.done: {
383                 final Intent extra = new Intent();
384                 extra.putExtra(PARAM_MEDIA_ITEM_ID,
385                         getIntent().getStringExtra(PARAM_MEDIA_ITEM_ID));
386                 extra.putExtra(PARAM_START_RECT, mStartRect);
387                 extra.putExtra(PARAM_END_RECT, mEndRect);
388                 setResult(RESULT_OK, extra);
389                 finish();
390                 break;
391             }
392 
393             default: {
394                 break;
395             }
396         }
397     }
398 
399     /**
400      * Show/hide the progress bar
401      *
402      * @param show true to show the progress
403      */
showProgress(boolean show)404     private void showProgress(boolean show) {
405         if (show) {
406             findViewById(R.id.image_loading).setVisibility(View.VISIBLE);
407         } else {
408             findViewById(R.id.image_loading).setVisibility(View.GONE);
409         }
410     }
411 
412     /**
413      * Enable the "Done" button if both rectangles are set
414      */
enableDoneButton()415     private void enableDoneButton() {
416         mDoneButton.setEnabled(!mStartRect.isEmpty() && !mEndRect.isEmpty());
417     }
418 
419     /**
420      * Show the bitmap rectangle
421      */
showBitmapRectangle()422     private void showBitmapRectangle() {
423         final int checkedRect = mRadioGroup.getCheckedRadioButtonId();
424         switch (checkedRect) {
425             case R.id.start_rectangle: {
426                 if (!mStartRect.isEmpty()) {
427                     mImageView.reset();
428                     final float scale = ((float)mMediaItemWidth)
429                             / ((float)(mStartRect.right - mStartRect.left));
430                     if (Log.isLoggable(TAG, Log.DEBUG)) {
431                         Log.d(TAG, "showBitmapRectangle START: " + scale + " "
432                                 + mStartRect.left + ", " + mStartRect.top + ", "
433                                 + mStartRect.right + ", " + mStartRect.bottom);
434                     }
435                     if (scale > 1F) {
436                         mImageView.zoomToOffset(scale, mStartRect.left * scale * mImageViewScale,
437                                 mStartRect.top * scale * mImageViewScale);
438                     }
439                 }
440                 break;
441             }
442 
443             case R.id.end_rectangle: {
444                 if (!mEndRect.isEmpty()) {
445                     mImageView.reset();
446                     final float scale = ((float)mMediaItemWidth)
447                             / ((float)(mEndRect.right - mEndRect.left));
448                     if (Log.isLoggable(TAG, Log.DEBUG)) {
449                         Log.d(TAG, "showBitmapRectangle END: " + scale + " "
450                                 + mEndRect.left + ", " + mEndRect.top + ", "
451                                 + mEndRect.right + ", " + mEndRect.bottom);
452                     }
453                     if (scale > 1F) {
454                         mImageView.zoomToOffset(scale, mEndRect.left * scale * mImageViewScale,
455                                 mEndRect.top * scale * mImageViewScale);
456                     }
457                 }
458                 break;
459             }
460 
461             default: {
462                 break;
463             }
464         }
465     }
466 
467     /**
468      * Show the bitmap rectangle
469      */
saveBitmapRectangle()470     private void saveBitmapRectangle() {
471         final int checkedRect = mRadioGroup.getCheckedRadioButtonId();
472         final FrameLayout.LayoutParams lp =
473             (FrameLayout.LayoutParams)mImageView.getLayoutParams();
474         switch (checkedRect) {
475             case R.id.start_rectangle: {
476                 mMatrixRect.set(0, 0, lp.width, lp.height);
477 
478                 mImageView.mapRect(mMatrixRect);
479                 final float scale = mImageView.getScale();
480 
481                 if (Log.isLoggable(TAG, Log.DEBUG)) {
482                     Log.d(TAG, "START RAW: " + scale + ", rect: " + mMatrixRect.left
483                             + ", " + mMatrixRect.top + ", " + mMatrixRect.right
484                             + ", " + mMatrixRect.bottom);
485                 }
486 
487                 final int left = (int)((-mMatrixRect.left/scale) / mImageViewScale);
488                 final int top = (int)((-mMatrixRect.top/scale) / mImageViewScale);
489                 final int right = (int)(((-mMatrixRect.left + lp.width)/scale) / mImageViewScale);
490                 final int bottom = (int)(((-mMatrixRect.top + lp.height)/scale) / mImageViewScale);
491 
492                 mStartRect.set(left, top, right, bottom);
493                 if (Log.isLoggable(TAG, Log.DEBUG)) {
494                     Log.d(TAG, "START: " + mStartRect.left + ", " + mStartRect.top + ", "
495                             + mStartRect.right + ", " + mStartRect.bottom);
496                 }
497 
498                 enableDoneButton();
499                 break;
500             }
501 
502             case R.id.end_rectangle: {
503                 mMatrixRect.set(0, 0, lp.width, lp.height);
504 
505                 mImageView.mapRect(mMatrixRect);
506                 final float scale = mImageView.getScale();
507 
508                 if (Log.isLoggable(TAG, Log.DEBUG)) {
509                     Log.d(TAG, "END RAW: " + scale + ", rect: " + mMatrixRect.left
510                             + ", " + mMatrixRect.top + ", " + mMatrixRect.right
511                             + ", " + mMatrixRect.bottom);
512                 }
513 
514                 final int left = (int)((-mMatrixRect.left/scale) / mImageViewScale);
515                 final int top = (int)((-mMatrixRect.top/scale) / mImageViewScale);
516                 final int right = (int)(((-mMatrixRect.left + lp.width)/scale) / mImageViewScale);
517                 final int bottom = (int)(((-mMatrixRect.top + lp.height)/scale) / mImageViewScale);
518 
519                 mEndRect.set(left, top, right, bottom);
520                 if (Log.isLoggable(TAG, Log.DEBUG)) {
521                     Log.d(TAG, "END: " + mEndRect.left + ", " + mEndRect.top + ", "
522                             + mEndRect.right + ", " + mEndRect.bottom);
523                 }
524 
525                 enableDoneButton();
526                 break;
527             }
528 
529             default: {
530                 break;
531             }
532         }
533     }
534 }
535