Home | History | Annotate | Download | only in videoeditor
      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