blob: 7f37c2adf2154829b3e5663e7579948e851b3e84 [file] [log] [blame]
/*
* 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.wallpaperpicker;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import com.android.photos.views.TiledImageRenderer.TileSource;
import com.android.photos.views.TiledImageView;
public class CropView extends TiledImageView implements OnScaleGestureListener {
private ScaleGestureDetector mScaleGestureDetector;
private long mTouchDownTime;
private float mFirstX, mFirstY;
private float mLastX, mLastY;
private float mCenterX, mCenterY;
private float mMinScale;
private boolean mTouchEnabled = true;
private RectF mTempEdges = new RectF();
private float[] mTempPoint = new float[] { 0, 0 };
private float[] mTempCoef = new float[] { 0, 0 };
private float[] mTempAdjustment = new float[] { 0, 0 };
private float[] mTempImageDims = new float[] { 0, 0 };
private float[] mTempRendererCenter = new float[] { 0, 0 };
TouchCallback mTouchCallback;
Matrix mRotateMatrix;
Matrix mInverseRotateMatrix;
public interface TouchCallback {
void onTouchDown();
void onTap();
void onTouchUp();
}
public CropView(Context context) {
this(context, null);
}
public CropView(Context context, AttributeSet attrs) {
super(context, attrs);
mScaleGestureDetector = new ScaleGestureDetector(context, this);
mRotateMatrix = new Matrix();
mInverseRotateMatrix = new Matrix();
}
private float[] getImageDims() {
final float imageWidth = mRenderer.source.getImageWidth();
final float imageHeight = mRenderer.source.getImageHeight();
float[] imageDims = mTempImageDims;
imageDims[0] = imageWidth;
imageDims[1] = imageHeight;
mRotateMatrix.mapPoints(imageDims);
imageDims[0] = Math.abs(imageDims[0]);
imageDims[1] = Math.abs(imageDims[1]);
return imageDims;
}
private void getEdgesHelper(RectF edgesOut) {
final float width = getWidth();
final float height = getHeight();
final float[] imageDims = getImageDims();
final float imageWidth = imageDims[0];
final float imageHeight = imageDims[1];
float initialCenterX = mRenderer.source.getImageWidth() / 2f;
float initialCenterY = mRenderer.source.getImageHeight() / 2f;
float[] rendererCenter = mTempRendererCenter;
rendererCenter[0] = mCenterX - initialCenterX;
rendererCenter[1] = mCenterY - initialCenterY;
mRotateMatrix.mapPoints(rendererCenter);
rendererCenter[0] += imageWidth / 2;
rendererCenter[1] += imageHeight / 2;
final float scale = mRenderer.scale;
float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f)
* scale + width / 2f;
float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f)
* scale + height / 2f;
float leftEdge = centerX - imageWidth / 2f * scale;
float rightEdge = centerX + imageWidth / 2f * scale;
float topEdge = centerY - imageHeight / 2f * scale;
float bottomEdge = centerY + imageHeight / 2f * scale;
edgesOut.left = leftEdge;
edgesOut.right = rightEdge;
edgesOut.top = topEdge;
edgesOut.bottom = bottomEdge;
}
public int getImageRotation() {
return mRenderer.rotation;
}
public RectF getCrop() {
final RectF edges = mTempEdges;
getEdgesHelper(edges);
final float scale = mRenderer.scale;
float cropLeft = -edges.left / scale;
float cropTop = -edges.top / scale;
float cropRight = cropLeft + getWidth() / scale;
float cropBottom = cropTop + getHeight() / scale;
return new RectF(cropLeft, cropTop, cropRight, cropBottom);
}
public Point getSourceDimensions() {
return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
}
public void setTileSource(TileSource source, Runnable isReadyCallback) {
super.setTileSource(source, isReadyCallback);
mCenterX = mRenderer.centerX;
mCenterY = mRenderer.centerY;
mRotateMatrix.reset();
mRotateMatrix.setRotate(mRenderer.rotation);
mInverseRotateMatrix.reset();
mInverseRotateMatrix.setRotate(-mRenderer.rotation);
updateMinScale(getWidth(), getHeight(), source, true);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
updateMinScale(w, h, mRenderer.source, false);
}
public void setScale(float scale) {
synchronized (mLock) {
mRenderer.scale = scale;
}
}
private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
synchronized (mLock) {
if (resetScale) {
mRenderer.scale = 1;
}
if (source != null) {
final float[] imageDims = getImageDims();
final float imageWidth = imageDims[0];
final float imageHeight = imageDims[1];
mMinScale = Math.max(w / imageWidth, h / imageHeight);
mRenderer.scale =
Math.max(mMinScale, resetScale ? Float.MIN_VALUE : mRenderer.scale);
}
}
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
// Don't need the lock because this will only fire inside of
// onTouchEvent
mRenderer.scale *= detector.getScaleFactor();
mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
invalidate();
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
/**
* Offsets wallpaper preview according to the state it will be displayed in upon returning home.
* @param offset Ranges from 0 to 1, where 0 is the leftmost parallax and 1 is the rightmost.
*/
public void setParallaxOffset(float offset, RectF crop) {
offset = Math.max(0, Math.min(offset, 1)); // Make sure the offset is in the correct range.
float screenWidth = getWidth() / mRenderer.scale;
mCenterX = screenWidth / 2 + offset * (crop.width() - screenWidth) + crop.left;
updateCenter();
}
public void moveToLeft() {
if (getWidth() == 0 || getHeight() == 0) {
final ViewTreeObserver observer = getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
moveToLeft();
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
final RectF edges = mTempEdges;
getEdgesHelper(edges);
final float scale = mRenderer.scale;
mCenterX += Math.ceil(edges.left / scale);
updateCenter();
}
private void updateCenter() {
mRenderer.centerX = Math.round(mCenterX);
mRenderer.centerY = Math.round(mCenterY);
}
public void setTouchEnabled(boolean enabled) {
mTouchEnabled = enabled;
}
public void setTouchCallback(TouchCallback cb) {
mTouchCallback = cb;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
final int skipIndex = pointerUp ? event.getActionIndex() : -1;
// Determine focal point
float sumX = 0, sumY = 0;
final int count = event.getPointerCount();
for (int i = 0; i < count; i++) {
if (skipIndex == i)
continue;
sumX += event.getX(i);
sumY += event.getY(i);
}
final int div = pointerUp ? count - 1 : count;
float x = sumX / div;
float y = sumY / div;
if (action == MotionEvent.ACTION_DOWN) {
mFirstX = x;
mFirstY = y;
mTouchDownTime = System.currentTimeMillis();
if (mTouchCallback != null) {
mTouchCallback.onTouchDown();
}
} else if (action == MotionEvent.ACTION_UP) {
ViewConfiguration config = ViewConfiguration.get(getContext());
float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
long now = System.currentTimeMillis();
if (mTouchCallback != null) {
// only do this if it's a small movement
if (squaredDist < slop &&
now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
mTouchCallback.onTap();
}
mTouchCallback.onTouchUp();
}
}
if (!mTouchEnabled) {
return true;
}
synchronized (mLock) {
mScaleGestureDetector.onTouchEvent(event);
switch (action) {
case MotionEvent.ACTION_MOVE:
float[] point = mTempPoint;
point[0] = (mLastX - x) / mRenderer.scale;
point[1] = (mLastY - y) / mRenderer.scale;
mInverseRotateMatrix.mapPoints(point);
mCenterX += point[0];
mCenterY += point[1];
updateCenter();
invalidate();
break;
}
if (mRenderer.source != null) {
// Adjust position so that the wallpaper covers the entire area
// of the screen
final RectF edges = mTempEdges;
getEdgesHelper(edges);
final float scale = mRenderer.scale;
float[] coef = mTempCoef;
coef[0] = 1;
coef[1] = 1;
mRotateMatrix.mapPoints(coef);
float[] adjustment = mTempAdjustment;
mTempAdjustment[0] = 0;
mTempAdjustment[1] = 0;
if (edges.left > 0) {
adjustment[0] = edges.left / scale;
} else if (edges.right < getWidth()) {
adjustment[0] = (edges.right - getWidth()) / scale;
}
if (edges.top > 0) {
adjustment[1] = (float) Math.ceil(edges.top / scale);
} else if (edges.bottom < getHeight()) {
adjustment[1] = (edges.bottom - getHeight()) / scale;
}
for (int dim = 0; dim <= 1; dim++) {
if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]);
}
mInverseRotateMatrix.mapPoints(adjustment);
mCenterX += adjustment[0];
mCenterY += adjustment[1];
updateCenter();
}
}
mLastX = x;
mLastY = y;
return true;
}
}