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.util.FloatMath;
25import android.view.MotionEvent;
26import android.view.ScaleGestureDetector;
27import android.view.ScaleGestureDetector.OnScaleGestureListener;
28import android.view.ViewConfiguration;
29import android.view.ViewTreeObserver;
30import android.view.ViewTreeObserver.OnGlobalLayoutListener;
31
32import com.android.photos.views.TiledImageRenderer.TileSource;
33import com.android.photos.views.TiledImageView;
34
35public class CropView extends TiledImageView implements OnScaleGestureListener {
36
37    private ScaleGestureDetector mScaleGestureDetector;
38    private long mTouchDownTime;
39    private float mFirstX, mFirstY;
40    private float mLastX, mLastY;
41    private float mCenterX, mCenterY;
42    private float mMinScale;
43    private boolean mTouchEnabled = true;
44    private RectF mTempEdges = new RectF();
45    private float[] mTempPoint = new float[] { 0, 0 };
46    private float[] mTempCoef = new float[] { 0, 0 };
47    private float[] mTempAdjustment = new float[] { 0, 0 };
48    private float[] mTempImageDims = new float[] { 0, 0 };
49    private float[] mTempRendererCenter = new float[] { 0, 0 };
50    TouchCallback mTouchCallback;
51    Matrix mRotateMatrix;
52    Matrix mInverseRotateMatrix;
53
54    public interface TouchCallback {
55        void onTouchDown();
56        void onTap();
57        void onTouchUp();
58    }
59
60    public CropView(Context context) {
61        this(context, null);
62    }
63
64    public CropView(Context context, AttributeSet attrs) {
65        super(context, attrs);
66        mScaleGestureDetector = new ScaleGestureDetector(context, this);
67        mRotateMatrix = new Matrix();
68        mInverseRotateMatrix = new Matrix();
69    }
70
71    private float[] getImageDims() {
72        final float imageWidth = mRenderer.source.getImageWidth();
73        final float imageHeight = mRenderer.source.getImageHeight();
74        float[] imageDims = mTempImageDims;
75        imageDims[0] = imageWidth;
76        imageDims[1] = imageHeight;
77        mRotateMatrix.mapPoints(imageDims);
78        imageDims[0] = Math.abs(imageDims[0]);
79        imageDims[1] = Math.abs(imageDims[1]);
80        return imageDims;
81    }
82
83    private void getEdgesHelper(RectF edgesOut) {
84        final float width = getWidth();
85        final float height = getHeight();
86        final float[] imageDims = getImageDims();
87        final float imageWidth = imageDims[0];
88        final float imageHeight = imageDims[1];
89
90        float initialCenterX = mRenderer.source.getImageWidth() / 2f;
91        float initialCenterY = mRenderer.source.getImageHeight() / 2f;
92
93        float[] rendererCenter = mTempRendererCenter;
94        rendererCenter[0] = mCenterX - initialCenterX;
95        rendererCenter[1] = mCenterY - initialCenterY;
96        mRotateMatrix.mapPoints(rendererCenter);
97        rendererCenter[0] += imageWidth / 2;
98        rendererCenter[1] += imageHeight / 2;
99
100        final float scale = mRenderer.scale;
101        float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f)
102                * scale + width / 2f;
103        float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f)
104                * scale + height / 2f;
105        float leftEdge = centerX - imageWidth / 2f * scale;
106        float rightEdge = centerX + imageWidth / 2f * scale;
107        float topEdge = centerY - imageHeight / 2f * scale;
108        float bottomEdge = centerY + imageHeight / 2f * scale;
109
110        edgesOut.left = leftEdge;
111        edgesOut.right = rightEdge;
112        edgesOut.top = topEdge;
113        edgesOut.bottom = bottomEdge;
114    }
115
116    public int getImageRotation() {
117        return mRenderer.rotation;
118    }
119
120    public RectF getCrop() {
121        final RectF edges = mTempEdges;
122        getEdgesHelper(edges);
123        final float scale = mRenderer.scale;
124
125        float cropLeft = -edges.left / scale;
126        float cropTop = -edges.top / scale;
127        float cropRight = cropLeft + getWidth() / scale;
128        float cropBottom = cropTop + getHeight() / scale;
129
130        return new RectF(cropLeft, cropTop, cropRight, cropBottom);
131    }
132
133    public Point getSourceDimensions() {
134        return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
135    }
136
137    public void setTileSource(TileSource source, Runnable isReadyCallback) {
138        super.setTileSource(source, isReadyCallback);
139        mCenterX = mRenderer.centerX;
140        mCenterY = mRenderer.centerY;
141        mRotateMatrix.reset();
142        mRotateMatrix.setRotate(mRenderer.rotation);
143        mInverseRotateMatrix.reset();
144        mInverseRotateMatrix.setRotate(-mRenderer.rotation);
145        updateMinScale(getWidth(), getHeight(), source, true);
146    }
147
148    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
149        updateMinScale(w, h, mRenderer.source, false);
150    }
151
152    public void setScale(float scale) {
153        synchronized (mLock) {
154            mRenderer.scale = scale;
155        }
156    }
157
158    private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
159        synchronized (mLock) {
160            if (resetScale) {
161                mRenderer.scale = 1;
162            }
163            if (source != null) {
164                final float[] imageDims = getImageDims();
165                final float imageWidth = imageDims[0];
166                final float imageHeight = imageDims[1];
167                mMinScale = Math.max(w / imageWidth, h / imageHeight);
168                mRenderer.scale = Math.max(mMinScale, 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] = FloatMath.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] = FloatMath.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