/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.videoeditor; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Rect; import android.graphics.RectF; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.view.ScaleGestureDetector.OnScaleGestureListener; import android.widget.FrameLayout; import android.widget.RadioGroup; import android.widget.Toast; import com.example.android.videoeditor.widgets.ImageViewTouchBase; /** * Activity for setting the begin and end Ken Burns viewing rectangles */ public class KenBurnsActivity extends Activity { // Logging private static final String TAG = "KenBurnsActivity"; // State keys private static final String STATE_WHICH_RECTANGLE_ID = "which"; private static final String STATE_START_RECTANGLE = "start"; private static final String STATE_END_RECTANGLE = "end"; // Intent extras public static final String PARAM_WIDTH = "width"; public static final String PARAM_HEIGHT = "height"; public static final String PARAM_FILENAME = "filename"; public static final String PARAM_MEDIA_ITEM_ID = "media_item_id"; public static final String PARAM_START_RECT = "start_rect"; public static final String PARAM_END_RECT = "end_rect"; private static final int MAX_HW_BITMAP_WIDTH = 2048; private static final int MAX_HW_BITMAP_HEIGHT = 2048; private static final int MAX_WIDTH = 1296; private static final int MAX_HEIGHT = 720; private static final int MAX_PAN = 3; // Instance variables private final Rect mStartRect = new Rect(0, 0, 0, 0); private final Rect mEndRect = new Rect(0, 0, 0, 0); private final RectF mMatrixRect = new RectF(0, 0, 0, 0); private RadioGroup mRadioGroup; private ImageViewTouchBase mImageView; private View mDoneButton; private GestureDetector mGestureDetector; private ScaleGestureDetector mScaleGestureDetector; private boolean mPaused = true; private int mMediaItemWidth, mMediaItemHeight; private float mImageViewScale; private int mImageSubsample; private Bitmap mBitmap; /** * The simple gestures listener */ private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (mImageView.getScale() > 1F) { mImageView.postTranslateCenter(-distanceX, -distanceY); saveBitmapRectangle(); } return true; } @Override public boolean onSingleTapUp(MotionEvent e) { return true; } @Override public boolean onDoubleTap(MotionEvent e) { // Switch between the original scale and 3x scale. if (mImageView.getScale() > 2F) { mImageView.zoomTo(1F); } else { mImageView.zoomTo(3F, e.getX(), e.getY()); } saveBitmapRectangle(); return true; } } /** * Scale gesture listener */ private class MyScaleGestureListener implements OnScaleGestureListener { @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } @Override public boolean onScale(ScaleGestureDetector detector) { final float relativeScaleFactor = detector.getScaleFactor(); final float newAbsoluteScale = relativeScaleFactor * mImageView.getScale(); if (newAbsoluteScale < 1.0F) { return false; } mImageView.zoomTo(newAbsoluteScale, detector.getFocusX(), detector.getFocusY()); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { saveBitmapRectangle(); } } /** * Image loader class */ private class ImageLoaderAsyncTask extends AsyncTask<Void, Void, Bitmap> { // Instance variables private final String mFilename; /** * Constructor * * @param filename The filename */ public ImageLoaderAsyncTask(String filename) { mFilename = filename; showProgress(true); } @Override protected Bitmap doInBackground(Void... zzz) { if (mPaused) { return null; } // Wait for the layout to complete while (mImageView.getWidth() <= 0) { try { Thread.sleep(30); } catch (InterruptedException ex) { } } if (mBitmap != null) { return mBitmap; } else { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = mImageSubsample; return BitmapFactory.decodeFile(mFilename, options); } } @Override protected void onPostExecute(Bitmap bitmap) { if (bitmap == null) { if (!mPaused) { finish(); } return; } if (!mPaused) { showProgress(false); mRadioGroup.setEnabled(true); mImageView.setImageBitmapResetBase(bitmap, true); mBitmap = bitmap; if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Bitmap size: " + bitmap.getWidth() + "x" + bitmap.getHeight() + ", bytes: " + (bitmap.getRowBytes() * bitmap.getHeight())); } showBitmapRectangle(); } } } @Override public void onCreate(Bundle state) { super.onCreate(state); setContentView(R.layout.ken_burns_layout); setFinishOnTouchOutside(true); mMediaItemWidth = getIntent().getIntExtra(PARAM_WIDTH, 0); mMediaItemHeight = getIntent().getIntExtra(PARAM_HEIGHT, 0); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Media item size: " + mMediaItemWidth + "x" + mMediaItemHeight); } // Setup the image view mImageView = (ImageViewTouchBase)findViewById(R.id.ken_burns_image); // Set the width and height of the image view final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams)mImageView.getLayoutParams(); if (mMediaItemWidth >= mMediaItemHeight) { lp.width = Math.min(mMediaItemWidth, MAX_WIDTH) / MAX_PAN; // Compute the height by preserving the aspect ratio lp.height = (lp.width * mMediaItemHeight) / mMediaItemWidth; mImageSubsample = mMediaItemWidth / (lp.width * MAX_PAN); } else { lp.height = Math.min(mMediaItemHeight, MAX_HEIGHT) / MAX_PAN; // Compute the width by preserving the aspect ratio lp.width = (lp.height * mMediaItemWidth) / mMediaItemHeight; mImageSubsample = mMediaItemHeight / (lp.height * MAX_PAN); } // Ensure that the size of the bitmap will not exceed the size supported // by HW vendors while ((mMediaItemWidth / mImageSubsample > MAX_HW_BITMAP_WIDTH) || (mMediaItemHeight / mImageSubsample > MAX_HW_BITMAP_HEIGHT)) { mImageSubsample++; } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "View size: " + lp.width + "x" + lp.height + ", subsample: " + mImageSubsample); } // If the image is too small the image view may be too small to pinch if (lp.width < 120 || lp.height < 120) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Image is too small: " + lp.width + "x" + lp.height); } Toast.makeText(this, getString(R.string.pan_zoom_small_image_error), Toast.LENGTH_LONG).show(); finish(); return; } mImageView.setLayoutParams(lp); mImageViewScale = ((float)lp.width) / ((float)mMediaItemWidth); mGestureDetector = new GestureDetector(this, new MyGestureListener()); mScaleGestureDetector = new ScaleGestureDetector(this, new MyScaleGestureListener()); mRadioGroup = (RadioGroup)findViewById(R.id.which_rectangle); if (state != null) { mRadioGroup.check(state.getInt(STATE_WHICH_RECTANGLE_ID)); mStartRect.set((Rect)state.getParcelable(STATE_START_RECTANGLE)); mEndRect.set((Rect)state.getParcelable(STATE_END_RECTANGLE)); } else { mRadioGroup.check(R.id.start_rectangle); final Rect startRect = (Rect)getIntent().getParcelableExtra(PARAM_START_RECT); if (startRect != null) { mStartRect.set(startRect); } else { mStartRect.set(0, 0, mMediaItemWidth, mMediaItemHeight); } final Rect endRect = (Rect)getIntent().getParcelableExtra(PARAM_END_RECT); if (endRect != null) { mEndRect.set(endRect); } else { mEndRect.set(0, 0, mMediaItemWidth, mMediaItemHeight); } } mDoneButton = findViewById(R.id.done); enableDoneButton(); // Disable the ratio buttons until we load the image mRadioGroup.setEnabled(false); mRadioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.start_rectangle: { showBitmapRectangle(); break; } case R.id.end_rectangle: { showBitmapRectangle(); break; } case R.id.done: { final Intent extra = new Intent(); extra.putExtra(PARAM_MEDIA_ITEM_ID, getIntent().getStringExtra(PARAM_MEDIA_ITEM_ID)); extra.putExtra(PARAM_START_RECT, mStartRect); extra.putExtra(PARAM_END_RECT, mEndRect); setResult(RESULT_OK, extra); finish(); break; } default: { break; } } } }); mBitmap = (Bitmap) getLastNonConfigurationInstance(); mImageView.setEventListener(new ImageViewTouchBase.ImageTouchEventListener() { @Override public boolean onImageTouchEvent(MotionEvent ev) { mScaleGestureDetector.onTouchEvent(ev); mGestureDetector.onTouchEvent(ev); return true; } }); } @Override protected void onResume() { super.onResume(); mPaused = false; // Load the image new ImageLoaderAsyncTask(getIntent().getStringExtra(PARAM_FILENAME)).execute(); } @Override protected void onPause() { super.onPause(); mPaused = true; } @Override protected void onDestroy() { super.onDestroy(); if (!isChangingConfigurations()) { if (mBitmap != null) { mBitmap.recycle(); mBitmap = null; } System.gc(); } } @Override public Object onRetainNonConfigurationInstance() { return mBitmap; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); final RadioGroup radioGroup = (RadioGroup)findViewById(R.id.which_rectangle); outState.putInt(STATE_WHICH_RECTANGLE_ID, radioGroup.getCheckedRadioButtonId()); outState.putParcelable(STATE_START_RECTANGLE, mStartRect); outState.putParcelable(STATE_END_RECTANGLE, mEndRect); } public void onClickHandler(View target) { switch (target.getId()) { case R.id.done: { final Intent extra = new Intent(); extra.putExtra(PARAM_MEDIA_ITEM_ID, getIntent().getStringExtra(PARAM_MEDIA_ITEM_ID)); extra.putExtra(PARAM_START_RECT, mStartRect); extra.putExtra(PARAM_END_RECT, mEndRect); setResult(RESULT_OK, extra); finish(); break; } default: { break; } } } /** * Show/hide the progress bar * * @param show true to show the progress */ private void showProgress(boolean show) { if (show) { findViewById(R.id.image_loading).setVisibility(View.VISIBLE); } else { findViewById(R.id.image_loading).setVisibility(View.GONE); } } /** * Enable the "Done" button if both rectangles are set */ private void enableDoneButton() { mDoneButton.setEnabled(!mStartRect.isEmpty() && !mEndRect.isEmpty()); } /** * Show the bitmap rectangle */ private void showBitmapRectangle() { final int checkedRect = mRadioGroup.getCheckedRadioButtonId(); switch (checkedRect) { case R.id.start_rectangle: { if (!mStartRect.isEmpty()) { mImageView.reset(); final float scale = ((float)mMediaItemWidth) / ((float)(mStartRect.right - mStartRect.left)); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "showBitmapRectangle START: " + scale + " " + mStartRect.left + ", " + mStartRect.top + ", " + mStartRect.right + ", " + mStartRect.bottom); } if (scale > 1F) { mImageView.zoomToOffset(scale, mStartRect.left * scale * mImageViewScale, mStartRect.top * scale * mImageViewScale); } } break; } case R.id.end_rectangle: { if (!mEndRect.isEmpty()) { mImageView.reset(); final float scale = ((float)mMediaItemWidth) / ((float)(mEndRect.right - mEndRect.left)); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "showBitmapRectangle END: " + scale + " " + mEndRect.left + ", " + mEndRect.top + ", " + mEndRect.right + ", " + mEndRect.bottom); } if (scale > 1F) { mImageView.zoomToOffset(scale, mEndRect.left * scale * mImageViewScale, mEndRect.top * scale * mImageViewScale); } } break; } default: { break; } } } /** * Show the bitmap rectangle */ private void saveBitmapRectangle() { final int checkedRect = mRadioGroup.getCheckedRadioButtonId(); final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams)mImageView.getLayoutParams(); switch (checkedRect) { case R.id.start_rectangle: { mMatrixRect.set(0, 0, lp.width, lp.height); mImageView.mapRect(mMatrixRect); final float scale = mImageView.getScale(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "START RAW: " + scale + ", rect: " + mMatrixRect.left + ", " + mMatrixRect.top + ", " + mMatrixRect.right + ", " + mMatrixRect.bottom); } final int left = (int)((-mMatrixRect.left/scale) / mImageViewScale); final int top = (int)((-mMatrixRect.top/scale) / mImageViewScale); final int right = (int)(((-mMatrixRect.left + lp.width)/scale) / mImageViewScale); final int bottom = (int)(((-mMatrixRect.top + lp.height)/scale) / mImageViewScale); mStartRect.set(left, top, right, bottom); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "START: " + mStartRect.left + ", " + mStartRect.top + ", " + mStartRect.right + ", " + mStartRect.bottom); } enableDoneButton(); break; } case R.id.end_rectangle: { mMatrixRect.set(0, 0, lp.width, lp.height); mImageView.mapRect(mMatrixRect); final float scale = mImageView.getScale(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "END RAW: " + scale + ", rect: " + mMatrixRect.left + ", " + mMatrixRect.top + ", " + mMatrixRect.right + ", " + mMatrixRect.bottom); } final int left = (int)((-mMatrixRect.left/scale) / mImageViewScale); final int top = (int)((-mMatrixRect.top/scale) / mImageViewScale); final int right = (int)(((-mMatrixRect.left + lp.width)/scale) / mImageViewScale); final int bottom = (int)(((-mMatrixRect.top + lp.height)/scale) / mImageViewScale); mEndRect.set(left, top, right, bottom); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "END: " + mEndRect.left + ", " + mEndRect.top + ", " + mEndRect.right + ", " + mEndRect.bottom); } enableDoneButton(); break; } default: { break; } } } }