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/* Copied from Launcher3 */
17package com.android.wallpapercropper;
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    public void moveToLeft() {
193        if (getWidth() == 0 || getHeight() == 0) {
194            final ViewTreeObserver observer = getViewTreeObserver();
195            observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
196                    public void onGlobalLayout() {
197                        moveToLeft();
198                        getViewTreeObserver().removeOnGlobalLayoutListener(this);
199                    }
200                });
201        }
202        final RectF edges = mTempEdges;
203        getEdgesHelper(edges);
204        final float scale = mRenderer.scale;
205        mCenterX += Math.ceil(edges.left / scale);
206        updateCenter();
207    }
208
209    private void updateCenter() {
210        mRenderer.centerX = Math.round(mCenterX);
211        mRenderer.centerY = Math.round(mCenterY);
212    }
213
214    public void setTouchEnabled(boolean enabled) {
215        mTouchEnabled = enabled;
216    }
217
218    public void setTouchCallback(TouchCallback cb) {
219        mTouchCallback = cb;
220    }
221
222    @Override
223    public boolean onTouchEvent(MotionEvent event) {
224        int action = event.getActionMasked();
225        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
226        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
227
228        // Determine focal point
229        float sumX = 0, sumY = 0;
230        final int count = event.getPointerCount();
231        for (int i = 0; i < count; i++) {
232            if (skipIndex == i)
233                continue;
234            sumX += event.getX(i);
235            sumY += event.getY(i);
236        }
237        final int div = pointerUp ? count - 1 : count;
238        float x = sumX / div;
239        float y = sumY / div;
240
241        if (action == MotionEvent.ACTION_DOWN) {
242            mFirstX = x;
243            mFirstY = y;
244            mTouchDownTime = System.currentTimeMillis();
245            if (mTouchCallback != null) {
246                mTouchCallback.onTouchDown();
247            }
248        } else if (action == MotionEvent.ACTION_UP) {
249            ViewConfiguration config = ViewConfiguration.get(getContext());
250
251            float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
252            float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
253            long now = System.currentTimeMillis();
254            if (mTouchCallback != null) {
255                // only do this if it's a small movement
256                if (squaredDist < slop &&
257                        now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
258                    mTouchCallback.onTap();
259                }
260                mTouchCallback.onTouchUp();
261            }
262        }
263
264        if (!mTouchEnabled) {
265            return true;
266        }
267
268        synchronized (mLock) {
269            mScaleGestureDetector.onTouchEvent(event);
270            switch (action) {
271                case MotionEvent.ACTION_MOVE:
272                    float[] point = mTempPoint;
273                    point[0] = (mLastX - x) / mRenderer.scale;
274                    point[1] = (mLastY - y) / mRenderer.scale;
275                    mInverseRotateMatrix.mapPoints(point);
276                    mCenterX += point[0];
277                    mCenterY += point[1];
278                    updateCenter();
279                    invalidate();
280                    break;
281            }
282            if (mRenderer.source != null) {
283                // Adjust position so that the wallpaper covers the entire area
284                // of the screen
285                final RectF edges = mTempEdges;
286                getEdgesHelper(edges);
287                final float scale = mRenderer.scale;
288
289                float[] coef = mTempCoef;
290                coef[0] = 1;
291                coef[1] = 1;
292                mRotateMatrix.mapPoints(coef);
293                float[] adjustment = mTempAdjustment;
294                mTempAdjustment[0] = 0;
295                mTempAdjustment[1] = 0;
296                if (edges.left > 0) {
297                    adjustment[0] = edges.left / scale;
298                } else if (edges.right < getWidth()) {
299                    adjustment[0] = (edges.right - getWidth()) / scale;
300                }
301                if (edges.top > 0) {
302                    adjustment[1] = (float) Math.ceil(edges.top / scale);
303                } else if (edges.bottom < getHeight()) {
304                    adjustment[1] = (edges.bottom - getHeight()) / scale;
305                }
306                for (int dim = 0; dim <= 1; dim++) {
307                    if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]);
308                }
309
310                mInverseRotateMatrix.mapPoints(adjustment);
311                mCenterX += adjustment[0];
312                mCenterY += adjustment[1];
313                updateCenter();
314            }
315        }
316
317        mLastX = x;
318        mLastY = y;
319        return true;
320    }
321}
322