CropView.java revision e72aa7f0a64ff55f747751b7972ccb8acebab7da
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.Point;
21import android.graphics.RectF;
22import android.util.AttributeSet;
23import android.view.MotionEvent;
24import android.view.ScaleGestureDetector;
25import android.view.ScaleGestureDetector.OnScaleGestureListener;
26import android.view.ViewConfiguration;
27import android.view.ViewTreeObserver;
28import android.view.ViewTreeObserver.OnGlobalLayoutListener;
29
30import com.android.photos.views.TiledImageRenderer.TileSource;
31import com.android.photos.views.TiledImageView;
32
33public class CropView extends TiledImageView implements OnScaleGestureListener {
34
35    private ScaleGestureDetector mScaleGestureDetector;
36    private long mTouchDownTime;
37    private float mFirstX, mFirstY;
38    private float mLastX, mLastY;
39    private float mMinScale;
40    private boolean mTouchEnabled = true;
41    private RectF mTempEdges = new RectF();
42    TouchCallback mTouchCallback;
43
44    public interface TouchCallback {
45        void onTouchDown();
46        void onTap();
47        void onTouchUp();
48    }
49
50    public CropView(Context context) {
51        this(context, null);
52    }
53
54    public CropView(Context context, AttributeSet attrs) {
55        super(context, attrs);
56        mScaleGestureDetector = new ScaleGestureDetector(context, this);
57    }
58
59    private void getEdgesHelper(RectF edgesOut) {
60        final float width = getWidth();
61        final float height = getHeight();
62        final float imageWidth = mRenderer.source.getImageWidth();
63        final float imageHeight = mRenderer.source.getImageHeight();
64        final float scale = mRenderer.scale;
65        float centerX = (width / 2f - mRenderer.centerX + (imageWidth - width) / 2f)
66                * scale + width / 2f;
67        float centerY = (height / 2f - mRenderer.centerY + (imageHeight - height) / 2f)
68                * scale + height / 2f;
69        float leftEdge = centerX - imageWidth / 2f * scale;
70        float rightEdge = centerX + imageWidth / 2f * scale;
71        float topEdge = centerY - imageHeight / 2f * scale;
72        float bottomEdge = centerY + imageHeight / 2f * scale;
73
74        edgesOut.left = leftEdge;
75        edgesOut.right = rightEdge;
76        edgesOut.top = topEdge;
77        edgesOut.bottom = bottomEdge;
78    }
79
80    public RectF getCrop() {
81        final RectF edges = mTempEdges;
82        getEdgesHelper(edges);
83        final float scale = mRenderer.scale;
84
85        float cropLeft = -edges.left / scale;
86        float cropTop = -edges.top / scale;
87        float cropRight = cropLeft + getWidth() / scale;
88        float cropBottom = cropTop + getHeight() / scale;
89
90        return new RectF(cropLeft, cropTop, cropRight, cropBottom);
91    }
92
93    public Point getSourceDimensions() {
94        return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
95    }
96
97    public void setTileSource(TileSource source, Runnable isReadyCallback) {
98        super.setTileSource(source, isReadyCallback);
99        updateMinScale(getWidth(), getHeight(), source, true);
100    }
101
102    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
103        updateMinScale(w, h, mRenderer.source, false);
104    }
105
106    public void setScale(float scale) {
107        synchronized (mLock) {
108            mRenderer.scale = scale;
109        }
110    }
111
112    private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
113        synchronized (mLock) {
114            if (resetScale) {
115                mRenderer.scale = 1;
116            }
117            if (source != null) {
118                mMinScale = Math.max(w / (float) source.getImageWidth(),
119                        h / (float) source.getImageHeight());
120                mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
121            }
122        }
123    }
124
125    @Override
126    public boolean onScaleBegin(ScaleGestureDetector detector) {
127        return true;
128    }
129
130    @Override
131    public boolean onScale(ScaleGestureDetector detector) {
132        // Don't need the lock because this will only fire inside of
133        // onTouchEvent
134        mRenderer.scale *= detector.getScaleFactor();
135        mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
136        invalidate();
137        return true;
138    }
139
140    @Override
141    public void onScaleEnd(ScaleGestureDetector detector) {
142    }
143
144    public void moveToLeft() {
145        if (getWidth() == 0 || getHeight() == 0) {
146            final ViewTreeObserver observer = getViewTreeObserver();
147            observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
148                    public void onGlobalLayout() {
149                        moveToLeft();
150                        getViewTreeObserver().removeOnGlobalLayoutListener(this);
151                    }
152                });
153        }
154        final RectF edges = mTempEdges;
155        getEdgesHelper(edges);
156        final float scale = mRenderer.scale;
157        mRenderer.centerX += Math.ceil(edges.left / scale);
158    }
159
160    public void setTouchEnabled(boolean enabled) {
161        mTouchEnabled = enabled;
162    }
163
164    public void setTouchCallback(TouchCallback cb) {
165        mTouchCallback = cb;
166    }
167
168    @Override
169    public boolean onTouchEvent(MotionEvent event) {
170        int action = event.getActionMasked();
171        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
172        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
173
174        // Determine focal point
175        float sumX = 0, sumY = 0;
176        final int count = event.getPointerCount();
177        for (int i = 0; i < count; i++) {
178            if (skipIndex == i)
179                continue;
180            sumX += event.getX(i);
181            sumY += event.getY(i);
182        }
183        final int div = pointerUp ? count - 1 : count;
184        float x = sumX / div;
185        float y = sumY / div;
186
187        if (action == MotionEvent.ACTION_DOWN) {
188            mFirstX = x;
189            mFirstY = y;
190            mTouchDownTime = System.currentTimeMillis();
191            if (mTouchCallback != null) {
192                mTouchCallback.onTouchDown();
193            }
194        } else if (action == MotionEvent.ACTION_UP) {
195            ViewConfiguration config = ViewConfiguration.get(getContext());
196
197            float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
198            float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
199            long now = System.currentTimeMillis();
200            if (mTouchCallback != null) {
201                // only do this if it's a small movement
202                if (squaredDist < slop &&
203                    now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
204                    mTouchCallback.onTap();
205                }
206                mTouchCallback.onTouchUp();
207            }
208        }
209
210        if (!mTouchEnabled) {
211            return true;
212        }
213
214        synchronized (mLock) {
215            mScaleGestureDetector.onTouchEvent(event);
216            switch (action) {
217                case MotionEvent.ACTION_MOVE:
218                    mRenderer.centerX += (mLastX - x) / mRenderer.scale;
219                    mRenderer.centerY += (mLastY - y) / mRenderer.scale;
220                    invalidate();
221                    break;
222            }
223            if (mRenderer.source != null) {
224                // Adjust position so that the wallpaper covers the entire area
225                // of the screen
226                final RectF edges = mTempEdges;
227                getEdgesHelper(edges);
228                final float scale = mRenderer.scale;
229                if (edges.left > 0) {
230                    mRenderer.centerX += Math.ceil(edges.left / scale);
231                }
232                if (edges.right < getWidth()) {
233                    mRenderer.centerX += (edges.right - getWidth()) / scale;
234                }
235                if (edges.top > 0) {
236                    mRenderer.centerY += Math.ceil(edges.top / scale);
237                }
238                if (edges.bottom < getHeight()) {
239                    mRenderer.centerY += (edges.bottom - getHeight()) / scale;
240                }
241            }
242        }
243
244        mLastX = x;
245        mLastY = y;
246        return true;
247    }
248}
249