1/*
2 * Copyright (C) 2010 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.gallery3d.photoeditor.actions;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.DashPathEffect;
22import android.graphics.Paint;
23import android.graphics.Path;
24import android.util.AttributeSet;
25import android.view.MotionEvent;
26
27import com.android.gallery3d.R;
28
29/**
30 * View that shows grids and handles touch-events to adjust angle of rotation.
31 */
32class RotateView extends FullscreenToolView {
33
34    /**
35     * Listens to rotate changes.
36     */
37    public interface OnRotateChangeListener {
38
39        void onAngleChanged(float degrees, boolean fromUser);
40
41        void onStartTrackingTouch();
42
43        void onStopTrackingTouch();
44    }
45
46    // All angles used are defined between PI and -PI.
47    private static final float MATH_PI = (float) Math.PI;
48    private static final float MATH_HALF_PI = MATH_PI / 2;
49    private static final float RADIAN_TO_DEGREE = 180f / MATH_PI;
50
51    private final Paint dashStrokePaint;
52    private final Path grids = new Path();
53    private final Path referenceLine = new Path();
54    private final int gridsColor;
55    private final int referenceColor;
56
57    private OnRotateChangeListener listener;
58    private boolean drawGrids;
59    private int centerX;
60    private int centerY;
61    private float maxRotatedAngle;
62    private float minRotatedAngle;
63    private float currentRotatedAngle;
64    private float lastRotatedAngle;
65    private float touchStartAngle;
66
67    public RotateView(Context context, AttributeSet attrs) {
68        super(context, attrs);
69
70        dashStrokePaint = new Paint();
71        dashStrokePaint.setAntiAlias(true);
72        dashStrokePaint.setStyle(Paint.Style.STROKE);
73        dashStrokePaint.setPathEffect(new DashPathEffect(new float[] {15.0f, 5.0f}, 1.0f));
74        dashStrokePaint.setStrokeWidth(2f);
75        gridsColor = context.getResources().getColor(R.color.translucent_white);
76        referenceColor = context.getResources().getColor(R.color.translucent_cyan);
77    }
78
79    public void setRotatedAngle(float degrees) {
80        refreshAngle(degrees, false);
81    }
82
83    /**
84     * Sets allowed degrees for rotation span before rotating the view.
85     */
86    public void setRotateSpan(float degrees) {
87        if (degrees >= 360f) {
88            maxRotatedAngle = Float.POSITIVE_INFINITY;
89        } else {
90            maxRotatedAngle = (degrees / RADIAN_TO_DEGREE) / 2;
91        }
92        minRotatedAngle = -maxRotatedAngle;
93    }
94
95    public void setOnRotateChangeListener(OnRotateChangeListener listener) {
96        this.listener = listener;
97    }
98
99    public void setDrawGrids(boolean drawGrids) {
100        this.drawGrids = drawGrids;
101        invalidate();
102    }
103
104    @Override
105    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
106        super.onSizeChanged(w, h, oldw, oldh);
107
108        centerX = w / 2;
109        centerY = h / 2;
110
111        // Make reference line long enough to cross the bounds diagonally after being rotated.
112        referenceLine.reset();
113        float radius = (float) Math.hypot(centerX, centerY);
114        float delta = radius - centerX;
115        referenceLine.moveTo(-delta, centerY);
116        referenceLine.lineTo(getWidth() + delta, centerY);
117        delta = radius - centerY;
118        referenceLine.moveTo(centerX, -delta);
119        referenceLine.lineTo(centerX, getHeight() + delta);
120
121        // Set grids inside photo display bounds.
122        grids.reset();
123        delta = displayBounds.width() / 4.0f;
124        for (float x = displayBounds.left + delta; x < displayBounds.right; x += delta) {
125            grids.moveTo(x, displayBounds.top);
126            grids.lineTo(x, displayBounds.bottom);
127        }
128        delta = displayBounds.height() / 4.0f;
129        for (float y = displayBounds.top + delta; y < displayBounds.bottom; y += delta) {
130            grids.moveTo(displayBounds.left, y);
131            grids.lineTo(displayBounds.right, y);
132        }
133    }
134
135    @Override
136    protected void onDraw(Canvas canvas) {
137        super.onDraw(canvas);
138
139        if (drawGrids) {
140            canvas.save();
141            canvas.clipRect(displayBounds);
142            dashStrokePaint.setColor(gridsColor);
143            canvas.drawPath(grids, dashStrokePaint);
144
145            canvas.rotate(-currentRotatedAngle * RADIAN_TO_DEGREE, centerX, centerY);
146            dashStrokePaint.setColor(referenceColor);
147            canvas.drawPath(referenceLine, dashStrokePaint);
148            canvas.restore();
149        }
150    }
151
152    private float calculateAngle(MotionEvent ev) {
153        float x = ev.getX() - centerX;
154        float y = centerY - ev.getY();
155
156        float angle;
157        if (x == 0) {
158            angle = (y >= 0) ? MATH_HALF_PI : -MATH_HALF_PI;
159        } else {
160            angle = (float) Math.atan(y / x);
161        }
162
163        if ((angle >= 0) && (x < 0)) {
164            angle = angle - MATH_PI;
165        } else if ((angle < 0) && (x < 0)) {
166            angle = MATH_PI + angle;
167        }
168        return angle;
169    }
170
171    @Override
172    public boolean onTouchEvent(MotionEvent ev) {
173        super.onTouchEvent(ev);
174
175        if (isEnabled()) {
176            switch (ev.getAction()) {
177                case MotionEvent.ACTION_DOWN:
178                    lastRotatedAngle = currentRotatedAngle;
179                    touchStartAngle = calculateAngle(ev);
180
181                    if (listener != null) {
182                        listener.onStartTrackingTouch();
183                    }
184                    break;
185
186                case MotionEvent.ACTION_MOVE:
187                    float touchAngle = calculateAngle(ev);
188                    float rotatedAngle = touchAngle - touchStartAngle + lastRotatedAngle;
189
190                    if ((rotatedAngle > maxRotatedAngle) || (rotatedAngle < minRotatedAngle)) {
191                        // Angles are out of range; restart rotating.
192                        // TODO: Fix discontinuity around boundary.
193                        lastRotatedAngle = currentRotatedAngle;
194                        touchStartAngle = touchAngle;
195                    } else {
196                        refreshAngle(-rotatedAngle * RADIAN_TO_DEGREE, true);
197                    }
198                    break;
199
200                case MotionEvent.ACTION_CANCEL:
201                case MotionEvent.ACTION_UP:
202                    if (listener != null) {
203                        listener.onStopTrackingTouch();
204                    }
205                    break;
206            }
207        }
208        return true;
209    }
210
211    private void refreshAngle(float degrees, boolean fromUser) {
212        currentRotatedAngle = -degrees / RADIAN_TO_DEGREE;
213        if (listener != null) {
214            listener.onAngleChanged(degrees, fromUser);
215        }
216        invalidate();
217    }
218}
219