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
17package com.android.videoeditor;
18
19import android.app.Activity;
20import android.content.Intent;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.graphics.Rect;
24import android.graphics.RectF;
25import android.os.AsyncTask;
26import android.os.Bundle;
27import android.util.Log;
28import android.view.GestureDetector;
29import android.view.MotionEvent;
30import android.view.ScaleGestureDetector;
31import android.view.View;
32import android.view.ScaleGestureDetector.OnScaleGestureListener;
33import android.widget.FrameLayout;
34import android.widget.RadioGroup;
35import android.widget.Toast;
36
37import com.android.videoeditor.widgets.ImageViewTouchBase;
38
39/**
40 * Activity for setting the begin and end Ken Burns viewing rectangles
41 */
42public 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