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