| /* |
| * Copyright (C) 2013 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.android.gallery3d.filtershow.crop; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.DashPathEffect; |
| import android.graphics.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.NinePatchDrawable; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.View; |
| |
| import com.android.gallery3d.R; |
| |
| |
| public class CropView extends View { |
| private static final String LOGTAG = "CropView"; |
| |
| private RectF mImageBounds = new RectF(); |
| private RectF mScreenBounds = new RectF(); |
| private RectF mScreenImageBounds = new RectF(); |
| private RectF mScreenCropBounds = new RectF(); |
| private Rect mShadowBounds = new Rect(); |
| |
| private Bitmap mBitmap; |
| private Paint mPaint = new Paint(); |
| |
| private NinePatchDrawable mShadow; |
| private CropObject mCropObj = null; |
| private Drawable mCropIndicator; |
| private int mIndicatorSize; |
| private int mRotation = 0; |
| private boolean mMovingBlock = false; |
| private Matrix mDisplayMatrix = null; |
| private Matrix mDisplayMatrixInverse = null; |
| private boolean mDirty = false; |
| |
| private float mPrevX = 0; |
| private float mPrevY = 0; |
| private float mSpotX = 0; |
| private float mSpotY = 0; |
| private boolean mDoSpot = false; |
| |
| private int mShadowMargin = 15; |
| private int mMargin = 32; |
| private int mOverlayShadowColor = 0xCF000000; |
| private int mOverlayWPShadowColor = 0x5F000000; |
| private int mWPMarkerColor = 0x7FFFFFFF; |
| private int mMinSideSize = 90; |
| private int mTouchTolerance = 40; |
| private float mDashOnLength = 20; |
| private float mDashOffLength = 10; |
| |
| private enum Mode { |
| NONE, MOVE |
| } |
| |
| private Mode mState = Mode.NONE; |
| |
| public CropView(Context context) { |
| super(context); |
| setup(context); |
| } |
| |
| public CropView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| setup(context); |
| } |
| |
| public CropView(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| setup(context); |
| } |
| |
| private void setup(Context context) { |
| Resources rsc = context.getResources(); |
| mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow); |
| mCropIndicator = rsc.getDrawable(R.drawable.camera_crop); |
| mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size); |
| mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin); |
| mMargin = (int) rsc.getDimension(R.dimen.preview_margin); |
| mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side); |
| mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance); |
| mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color); |
| mOverlayWPShadowColor = (int) rsc.getColor(R.color.crop_shadow_wp_color); |
| mWPMarkerColor = (int) rsc.getColor(R.color.crop_wp_markers); |
| mDashOnLength = rsc.getDimension(R.dimen.wp_selector_dash_length); |
| mDashOffLength = rsc.getDimension(R.dimen.wp_selector_off_length); |
| } |
| |
| public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) { |
| mBitmap = image; |
| if (mCropObj != null) { |
| RectF crop = mCropObj.getInnerBounds(); |
| RectF containing = mCropObj.getOuterBounds(); |
| if (crop != newCropBounds || containing != newPhotoBounds |
| || mRotation != rotation) { |
| mRotation = rotation; |
| mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds); |
| clearDisplay(); |
| } |
| } else { |
| mRotation = rotation; |
| mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0); |
| clearDisplay(); |
| } |
| } |
| |
| public RectF getCrop() { |
| return mCropObj.getInnerBounds(); |
| } |
| |
| public RectF getPhoto() { |
| return mCropObj.getOuterBounds(); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| float x = event.getX(); |
| float y = event.getY(); |
| if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { |
| return true; |
| } |
| float[] touchPoint = { |
| x, y |
| }; |
| mDisplayMatrixInverse.mapPoints(touchPoint); |
| x = touchPoint[0]; |
| y = touchPoint[1]; |
| switch (event.getActionMasked()) { |
| case (MotionEvent.ACTION_DOWN): |
| if (mState == Mode.NONE) { |
| if (!mCropObj.selectEdge(x, y)) { |
| mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK); |
| } |
| mPrevX = x; |
| mPrevY = y; |
| mState = Mode.MOVE; |
| } |
| break; |
| case (MotionEvent.ACTION_UP): |
| if (mState == Mode.MOVE) { |
| mCropObj.selectEdge(CropObject.MOVE_NONE); |
| mMovingBlock = false; |
| mPrevX = x; |
| mPrevY = y; |
| mState = Mode.NONE; |
| } |
| break; |
| case (MotionEvent.ACTION_MOVE): |
| if (mState == Mode.MOVE) { |
| float dx = x - mPrevX; |
| float dy = y - mPrevY; |
| mCropObj.moveCurrentSelection(dx, dy); |
| mPrevX = x; |
| mPrevY = y; |
| } |
| break; |
| default: |
| break; |
| } |
| invalidate(); |
| return true; |
| } |
| |
| private void reset() { |
| Log.w(LOGTAG, "crop reset called"); |
| mState = Mode.NONE; |
| mCropObj = null; |
| mRotation = 0; |
| mMovingBlock = false; |
| clearDisplay(); |
| } |
| |
| private void clearDisplay() { |
| mDisplayMatrix = null; |
| mDisplayMatrixInverse = null; |
| invalidate(); |
| } |
| |
| protected void configChanged() { |
| mDirty = true; |
| } |
| |
| public void applyFreeAspect() { |
| mCropObj.unsetAspectRatio(); |
| invalidate(); |
| } |
| |
| public void applyOriginalAspect() { |
| RectF outer = mCropObj.getOuterBounds(); |
| float w = outer.width(); |
| float h = outer.height(); |
| if (w > 0 && h > 0) { |
| applyAspect(w, h); |
| mCropObj.resetBoundsTo(outer, outer); |
| } else { |
| Log.w(LOGTAG, "failed to set aspect ratio original"); |
| } |
| } |
| |
| public void applySquareAspect() { |
| applyAspect(1, 1); |
| } |
| |
| public void applyAspect(float x, float y) { |
| if (x <= 0 || y <= 0) { |
| throw new IllegalArgumentException("Bad arguments to applyAspect"); |
| } |
| // If we are rotated by 90 degrees from horizontal, swap x and y |
| if (((mRotation < 0) ? -mRotation : mRotation) % 180 == 90) { |
| float tmp = x; |
| x = y; |
| y = tmp; |
| } |
| if (!mCropObj.setInnerAspectRatio(x, y)) { |
| Log.w(LOGTAG, "failed to set aspect ratio"); |
| } |
| invalidate(); |
| } |
| |
| public void setWallpaperSpotlight(float spotlightX, float spotlightY) { |
| mSpotX = spotlightX; |
| mSpotY = spotlightY; |
| if (mSpotX > 0 && mSpotY > 0) { |
| mDoSpot = true; |
| } |
| } |
| |
| public void unsetWallpaperSpotlight() { |
| mDoSpot = false; |
| } |
| |
| /** |
| * Rotates first d bits in integer x to the left some number of times. |
| */ |
| private int bitCycleLeft(int x, int times, int d) { |
| int mask = (1 << d) - 1; |
| int mout = x & mask; |
| times %= d; |
| int hi = mout >> (d - times); |
| int low = (mout << times) & mask; |
| int ret = x & ~mask; |
| ret |= low; |
| ret |= hi; |
| return ret; |
| } |
| |
| /** |
| * Find the selected edge or corner in screen coordinates. |
| */ |
| private int decode(int movingEdges, float rotation) { |
| int rot = CropMath.constrainedRotation(rotation); |
| switch (rot) { |
| case 90: |
| return bitCycleLeft(movingEdges, 1, 4); |
| case 180: |
| return bitCycleLeft(movingEdges, 2, 4); |
| case 270: |
| return bitCycleLeft(movingEdges, 3, 4); |
| default: |
| return movingEdges; |
| } |
| } |
| |
| @Override |
| public void onDraw(Canvas canvas) { |
| if (mBitmap == null) { |
| return; |
| } |
| if (mDirty) { |
| mDirty = false; |
| clearDisplay(); |
| } |
| |
| mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); |
| mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()); |
| mScreenBounds.inset(mMargin, mMargin); |
| |
| // If crop object doesn't exist, create it and update it from master |
| // state |
| if (mCropObj == null) { |
| reset(); |
| mCropObj = new CropObject(mImageBounds, mImageBounds, 0); |
| } |
| |
| // If display matrix doesn't exist, create it and its dependencies |
| if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { |
| mDisplayMatrix = new Matrix(); |
| mDisplayMatrix.reset(); |
| if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds, |
| mRotation)) { |
| Log.w(LOGTAG, "failed to get screen matrix"); |
| mDisplayMatrix = null; |
| return; |
| } |
| mDisplayMatrixInverse = new Matrix(); |
| mDisplayMatrixInverse.reset(); |
| if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) { |
| Log.w(LOGTAG, "could not invert display matrix"); |
| mDisplayMatrixInverse = null; |
| return; |
| } |
| // Scale min side and tolerance by display matrix scale factor |
| mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); |
| mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); |
| } |
| |
| mScreenImageBounds.set(mImageBounds); |
| |
| // Draw background shadow |
| if (mDisplayMatrix.mapRect(mScreenImageBounds)) { |
| int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin); |
| mScreenImageBounds.roundOut(mShadowBounds); |
| mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top - |
| margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin); |
| mShadow.setBounds(mShadowBounds); |
| mShadow.draw(canvas); |
| } |
| |
| mPaint.setAntiAlias(true); |
| mPaint.setFilterBitmap(true); |
| // Draw actual bitmap |
| canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint); |
| |
| mCropObj.getInnerBounds(mScreenCropBounds); |
| |
| if (mDisplayMatrix.mapRect(mScreenCropBounds)) { |
| |
| // Draw overlay shadows |
| Paint p = new Paint(); |
| p.setColor(mOverlayShadowColor); |
| p.setStyle(Paint.Style.FILL); |
| CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds); |
| |
| // Draw crop rect and markers |
| CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds); |
| if (!mDoSpot) { |
| CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds); |
| } else { |
| Paint wpPaint = new Paint(); |
| wpPaint.setColor(mWPMarkerColor); |
| wpPaint.setStrokeWidth(3); |
| wpPaint.setStyle(Paint.Style.STROKE); |
| wpPaint.setPathEffect(new DashPathEffect(new float[] |
| {mDashOnLength, mDashOnLength + mDashOffLength}, 0)); |
| p.setColor(mOverlayWPShadowColor); |
| CropDrawingUtils.drawWallpaperSelectionFrame(canvas, mScreenCropBounds, |
| mSpotX, mSpotY, wpPaint, p); |
| } |
| CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, |
| mScreenCropBounds, mCropObj.isFixedAspect(), decode(mCropObj.getSelectState(), mRotation)); |
| } |
| |
| } |
| } |