1/*
2 * Copyright (C) 2013 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.wallpaperpicker;
18
19import android.content.Context;
20import android.graphics.Matrix;
21import android.graphics.Point;
22import android.graphics.RectF;
23import android.util.AttributeSet;
24import android.view.MotionEvent;
25import android.view.ScaleGestureDetector;
26import android.view.ScaleGestureDetector.OnScaleGestureListener;
27import android.view.ViewConfiguration;
28import android.view.ViewTreeObserver;
29import android.view.ViewTreeObserver.OnGlobalLayoutListener;
30
31import com.android.photos.views.TiledImageRenderer.TileSource;
32import com.android.photos.views.TiledImageView;
33
34public class CropView extends TiledImageView implements OnScaleGestureListener {
35
36    private ScaleGestureDetector mScaleGestureDetector;
37    private long mTouchDownTime;
38    private float mFirstX, mFirstY;
39    private float mLastX, mLastY;
40    private float mCenterX, mCenterY;
41    private float mMinScale;
42    private boolean mTouchEnabled = true;
43    private RectF mTempEdges = new RectF();
44    private float[] mTempPoint = new float[] { 0, 0 };
45    private float[] mTempCoef = new float[] { 0, 0 };
46    private float[] mTempAdjustment = new float[] { 0, 0 };
47    private float[] mTempImageDims = new float[] { 0, 0 };
48    private float[] mTempRendererCenter = new float[] { 0, 0 };
49    TouchCallback mTouchCallback;
50    Matrix mRotateMatrix;
51    Matrix mInverseRotateMatrix;
52
53    public interface TouchCallback {
54        void onTouchDown();
55        void onTap();
56        void onTouchUp();
57    }
58
59    public CropView(Context context) {
60        this(context, null);
61    }
62
63    public CropView(Context context, AttributeSet attrs) {
64        super(context, attrs);
65        mScaleGestureDetector = new ScaleGestureDetector(context, this);
66        mRotateMatrix = new Matrix();
67        mInverseRotateMatrix = new Matrix();
68    }
69
70    private float[] getImageDims() {
71        final float imageWidth = mRenderer.source.getImageWidth();
72        final float imageHeight = mRenderer.source.getImageHeight();
73        float[] imageDims = mTempImageDims;
74        imageDims[0] = imageWidth;
75        imageDims[1] = imageHeight;
76        mRotateMatrix.mapPoints(imageDims);
77        imageDims[0] = Math.abs(imageDims[0]);
78        imageDims[1] = Math.abs(imageDims[1]);
79        return imageDims;
80    }
81
82    private void getEdgesHelper(RectF edgesOut) {
83        final float width = getWidth();
84        final float height = getHeight();
85        final float[] imageDims = getImageDims();
86        final float imageWidth = imageDims[0];
87        final float imageHeight = imageDims[1];
88
89        float initialCenterX = mRenderer.source.getImageWidth() / 2f;
90        float initialCenterY = mRenderer.source.getImageHeight() / 2f;
91
92        float[] rendererCenter = mTempRendererCenter;
93        rendererCenter[0] = mCenterX - initialCenterX;
94        rendererCenter[1] = mCenterY - initialCenterY;
95        mRotateMatrix.mapPoints(rendererCenter);
96        rendererCenter[0] += imageWidth / 2;
97        rendererCenter[1] += imageHeight / 2;
98
99        final float scale = mRenderer.scale;
100        float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f)
101                * scale + width / 2f;
102        float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f)
103                * scale + height / 2f;
104        float leftEdge = centerX - imageWidth / 2f * scale;
105        float rightEdge = centerX + imageWidth / 2f * scale;
106        float topEdge = centerY - imageHeight / 2f * scale;
107        float bottomEdge = centerY + imageHeight / 2f * scale;
108
109        edgesOut.left = leftEdge;
110        edgesOut.right = rightEdge;
111        edgesOut.top = topEdge;
112        edgesOut.bottom = bottomEdge;
113    }
114
115    public int getImageRotation() {
116        return mRenderer.rotation;
117    }
118
119    public RectF getCrop() {
120        final RectF edges = mTempEdges;
121        getEdgesHelper(edges);
122        final float scale = mRenderer.scale;
123
124        float cropLeft = -edges.left / scale;
125        float cropTop = -edges.top / scale;
126        float cropRight = cropLeft + getWidth() / scale;
127        float cropBottom = cropTop + getHeight() / scale;
128
129        return new RectF(cropLeft, cropTop, cropRight, cropBottom);
130    }
131
132    public Point getSourceDimensions() {
133        return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
134    }
135
136    public void setTileSource(TileSource source, Runnable isReadyCallback) {
137        super.setTileSource(source, isReadyCallback);
138        mCenterX = mRenderer.centerX;
139        mCenterY = mRenderer.centerY;
140        mRotateMatrix.reset();
141        mRotateMatrix.setRotate(mRenderer.rotation);
142        mInverseRotateMatrix.reset();
143        mInverseRotateMatrix.setRotate(-mRenderer.rotation);
144        updateMinScale(getWidth(), getHeight(), source, true);
145    }
146
147    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
148        updateMinScale(w, h, mRenderer.source, false);
149    }
150
151    public void setScale(float scale) {
152        synchronized (mLock) {
153            mRenderer.scale = scale;
154        }
155    }
156
157    private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
158        synchronized (mLock) {
159            if (resetScale) {
160                mRenderer.scale = 1;
161            }
162            if (source != null) {
163                final float[] imageDims = getImageDims();
164                final float imageWidth = imageDims[0];
165                final float imageHeight = imageDims[1];
166                mMinScale = Math.max(w / imageWidth, h / imageHeight);
167                mRenderer.scale =
168                        Math.max(mMinScale, resetScale ? Float.MIN_VALUE : mRenderer.scale);
169            }
170        }
171    }
172
173    @Override
174    public boolean onScaleBegin(ScaleGestureDetector detector) {
175        return true;
176    }
177
178    @Override
179    public boolean onScale(ScaleGestureDetector detector) {
180        // Don't need the lock because this will only fire inside of
181        // onTouchEvent
182        mRenderer.scale *= detector.getScaleFactor();
183        mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
184        invalidate();
185        return true;
186    }
187
188    @Override
189    public void onScaleEnd(ScaleGestureDetector detector) {
190    }
191
192    /**
193     * Offsets wallpaper preview according to the state it will be displayed in upon returning home.
194     * @param offset Ranges from 0 to 1, where 0 is the leftmost parallax and 1 is the rightmost.
195     */
196    public void setParallaxOffset(float offset, RectF crop) {
197        offset = Math.max(0, Math.min(offset, 1)); // Make sure the offset is in the correct range.
198        float screenWidth = getWidth() / mRenderer.scale;
199        mCenterX = screenWidth / 2 + offset * (crop.width() - screenWidth) + crop.left;
200        updateCenter();
201    }
202
203    public void moveToLeft() {
204        if (getWidth() == 0 || getHeight() == 0) {
205            final ViewTreeObserver observer = getViewTreeObserver();
206            observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
207                    public void onGlobalLayout() {
208                        moveToLeft();
209                        getViewTreeObserver().removeOnGlobalLayoutListener(this);
210                    }
211                });
212        }
213        final RectF edges = mTempEdges;
214        getEdgesHelper(edges);
215        final float scale = mRenderer.scale;
216        mCenterX += Math.ceil(edges.left / scale);
217        updateCenter();
218    }
219
220    private void updateCenter() {
221        mRenderer.centerX = Math.round(mCenterX);
222        mRenderer.centerY = Math.round(mCenterY);
223    }
224
225    public void setTouchEnabled(boolean enabled) {
226        mTouchEnabled = enabled;
227    }
228
229    public void setTouchCallback(TouchCallback cb) {
230        mTouchCallback = cb;
231    }
232
233    @Override
234    public boolean onTouchEvent(MotionEvent event) {
235        int action = event.getActionMasked();
236        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
237        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
238
239        // Determine focal point
240        float sumX = 0, sumY = 0;
241        final int count = event.getPointerCount();
242        for (int i = 0; i < count; i++) {
243            if (skipIndex == i)
244                continue;
245            sumX += event.getX(i);
246            sumY += event.getY(i);
247        }
248        final int div = pointerUp ? count - 1 : count;
249        float x = sumX / div;
250        float y = sumY / div;
251
252        if (action == MotionEvent.ACTION_DOWN) {
253            mFirstX = x;
254            mFirstY = y;
255            mTouchDownTime = System.currentTimeMillis();
256            if (mTouchCallback != null) {
257                mTouchCallback.onTouchDown();
258            }
259        } else if (action == MotionEvent.ACTION_UP) {
260            ViewConfiguration config = ViewConfiguration.get(getContext());
261
262            float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
263            float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
264            long now = System.currentTimeMillis();
265            if (mTouchCallback != null) {
266                // only do this if it's a small movement
267                if (squaredDist < slop &&
268                        now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
269                    mTouchCallback.onTap();
270                }
271                mTouchCallback.onTouchUp();
272            }
273        }
274
275        if (!mTouchEnabled) {
276            return true;
277        }
278
279        synchronized (mLock) {
280            mScaleGestureDetector.onTouchEvent(event);
281            switch (action) {
282                case MotionEvent.ACTION_MOVE:
283                    float[] point = mTempPoint;
284                    point[0] = (mLastX - x) / mRenderer.scale;
285                    point[1] = (mLastY - y) / mRenderer.scale;
286                    mInverseRotateMatrix.mapPoints(point);
287                    mCenterX += point[0];
288                    mCenterY += point[1];
289                    updateCenter();
290                    invalidate();
291                    break;
292            }
293            if (mRenderer.source != null) {
294                // Adjust position so that the wallpaper covers the entire area
295                // of the screen
296                final RectF edges = mTempEdges;
297                getEdgesHelper(edges);
298                final float scale = mRenderer.scale;
299
300                float[] coef = mTempCoef;
301                coef[0] = 1;
302                coef[1] = 1;
303                mRotateMatrix.mapPoints(coef);
304                float[] adjustment = mTempAdjustment;
305                mTempAdjustment[0] = 0;
306                mTempAdjustment[1] = 0;
307                if (edges.left > 0) {
308                    adjustment[0] = edges.left / scale;
309                } else if (edges.right < getWidth()) {
310                    adjustment[0] = (edges.right - getWidth()) / scale;
311                }
312                if (edges.top > 0) {
313                    adjustment[1] = (float) Math.ceil(edges.top / scale);
314                } else if (edges.bottom < getHeight()) {
315                    adjustment[1] = (edges.bottom - getHeight()) / scale;
316                }
317                for (int dim = 0; dim <= 1; dim++) {
318                    if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]);
319                }
320
321                mInverseRotateMatrix.mapPoints(adjustment);
322                mCenterX += adjustment[0];
323                mCenterY += adjustment[1];
324                updateCenter();
325            }
326        }
327
328        mLastX = x;
329        mLastY = y;
330        return true;
331    }
332}
333