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.videoeditor.widgets;
18
19import com.android.videoeditor.R;
20
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.drawable.Drawable;
24import android.util.AttributeSet;
25import android.view.MotionEvent;
26import android.view.View;
27
28/**
29 * The zoom control
30 */
31public class ZoomControl extends View {
32
33    private static final double MAX_ANGLE = Math.PI / 3;
34    private static final double THUMB_RADIUS_CONTAINER_SIZE_RATIO = 0.432;
35    private static final double THUMB_INTERNAL_RADIUS_CONTAINER_SIZE_RATIO = 0.24;
36
37    // Instance variables
38    private final Drawable mThumb;
39    private double mRadius;
40    private double mInternalRadius;
41    private int mMaxProgress, mProgress;
42    private OnZoomChangeListener mListener;
43    private int mThumbX, mThumbY;
44    private double mInterval;
45
46    /**
47     * The zoom change listener
48     */
49    public interface OnZoomChangeListener {
50        /**
51         * The progress value has changed
52         *
53         * @param progress The progress value
54         * @param fromUser true if the user is changing the zoom
55         */
56        public void onProgressChanged(int progress, boolean fromUser);
57    }
58
59    public ZoomControl(Context context, AttributeSet attrs, int defStyle) {
60        super(context, attrs, defStyle);
61
62        // Set the default maximum progress
63        mMaxProgress = 100;
64        computeInterval();
65
66        // Load the thumb selector
67        mThumb = context.getResources().getDrawable(R.drawable.zoom_thumb_selector);
68    }
69
70    @Override
71    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
72      super.onLayout(changed, left, top, right, bottom);
73      double width = right - left;
74      mRadius = width * THUMB_RADIUS_CONTAINER_SIZE_RATIO;
75      mInternalRadius = width * THUMB_INTERNAL_RADIUS_CONTAINER_SIZE_RATIO;
76    }
77
78    public ZoomControl(Context context, AttributeSet attrs) {
79        this(context, attrs, 0);
80    }
81
82    public ZoomControl(Context context) {
83        this(context, null, 0);
84    }
85
86    @Override
87    public void refreshDrawableState() {
88        mThumb.setState(isPressed() ? PRESSED_WINDOW_FOCUSED_STATE_SET : ENABLED_STATE_SET);
89        invalidate();
90    }
91
92    /**
93     * @param max The maximum value
94     */
95    public void setMax(int max) {
96        mMaxProgress = max;
97        computeInterval();
98    }
99
100    /**
101     * @param progress The progress
102     */
103    public void setProgress(int progress) {
104        mProgress = progress;
105
106        progressToPosition();
107        invalidate();
108    }
109
110    /**
111     * @param listener The listener
112     */
113    public void setOnZoomChangeListener(OnZoomChangeListener listener) {
114        mListener = listener;
115    }
116
117    @Override
118    protected void onDraw(Canvas canvas) {
119        super.onDraw(canvas);
120
121        if (mThumbX == 0 && mThumbY == 0) {
122            progressToPosition();
123        }
124
125        final int halfWidth = mThumb.getIntrinsicWidth() / 2;
126        final int halfHeight = mThumb.getIntrinsicHeight() / 2;
127        mThumb.setBounds(mThumbX - halfWidth, mThumbY - halfHeight, mThumbX + halfWidth,
128                mThumbY + halfHeight);
129        mThumb.setAlpha(isEnabled() ? 255 : 100);
130        mThumb.draw(canvas);
131    }
132
133    @Override
134    public boolean onTouchEvent(MotionEvent ev) {
135        super.onTouchEvent(ev);
136        switch (ev.getAction()) {
137            case MotionEvent.ACTION_DOWN: {
138                if (isEnabled()) {
139                    getParent().requestDisallowInterceptTouchEvent(true);
140                }
141                break;
142            }
143
144            case MotionEvent.ACTION_MOVE: {
145                if (isEnabled()) {
146                    final float x = ev.getX() - (getWidth() / 2);
147                    final float y = -(ev.getY() - (getHeight() / 2));
148                    final double alpha = Math.atan((double)y / (double)x);
149
150                    if (!checkHit(x, y, alpha)) {
151                        return true;
152                    }
153
154                    final int progress;
155                    if (x >= 0 && y >= 0) {
156                        mThumbX = (int)((mRadius * Math.cos(alpha)) + (getWidth() / 2));
157                        mThumbY = (int)((getHeight() / 2) - (mRadius * Math.sin(alpha)));
158                        progress = (int)((mMaxProgress / 2) - (alpha / mInterval));
159                    } else if (x >= 0 && y <= 0) {
160                        mThumbX = (int)((mRadius * Math.cos(alpha)) + (getWidth() / 2));
161                        mThumbY = (int)((getHeight() / 2) - (mRadius * Math.sin(alpha)));
162                        progress = (int)((mMaxProgress / 2) - (alpha / mInterval));
163                    } else if (x <= 0 && y >= 0) {
164                        mThumbX = (int)((getWidth() / 2) - (mRadius * Math.cos(alpha)));
165                        mThumbY = (int)((getHeight() / 2) + (mRadius * Math.sin(alpha)));
166                        progress = -(int)(((alpha + MAX_ANGLE) / mInterval));
167                    } else {
168                        mThumbX = (int)((getWidth() / 2) - (mRadius * Math.cos(alpha)));
169                        mThumbY = (int)((getHeight() / 2) + (mRadius * Math.sin(alpha)));
170                        progress = (int)(mMaxProgress - ((alpha - MAX_ANGLE) / mInterval));
171                    }
172
173                    invalidate();
174
175                    if (mListener != null) {
176                        if (progress != mProgress) {
177                            mProgress = progress;
178                            mListener.onProgressChanged(mProgress, true);
179                        }
180                    }
181                }
182                break;
183            }
184
185            case MotionEvent.ACTION_CANCEL:
186            case MotionEvent.ACTION_UP: {
187                break;
188            }
189
190            default: {
191                break;
192            }
193        }
194
195        return true;
196    }
197
198    /**
199     * Check if the user is touching the correct area
200     *
201     * @param x The horizontal coordinate
202     * @param y The vertical coordinate
203     * @param alpha The angle
204     * @return true if there is a hit in the allowed area
205     */
206    private boolean checkHit(float x, float y, double alpha) {
207        final double radius = Math.sqrt((x * x) + (y * y));
208        if (radius < mInternalRadius) {
209            return false;
210        }
211
212        if (x >= 0) {
213            return true;
214        } else if (y >= 0) {
215            if ((alpha >= -(Math.PI / 2)) && (alpha <= -MAX_ANGLE)) {
216                return true;
217            }
218        } else {
219            if ((alpha >= MAX_ANGLE) && (alpha <= (Math.PI / 2))) {
220                return true;
221            }
222        }
223
224        return false;
225    }
226
227    /**
228     * Compute the position of the thumb based on the progress values
229     */
230    private void progressToPosition() {
231        if (getWidth() == 0) { // Layout is not yet complete
232            return;
233        }
234
235        final double beta;
236        if (mProgress <= mMaxProgress / 2) {
237            beta = ((mMaxProgress / 2) - mProgress) * mInterval;
238        } else {
239            beta = ((mMaxProgress - mProgress) * mInterval) + Math.PI + MAX_ANGLE;
240        }
241
242        final double alpha;
243        if (beta >= 0 && beta <= Math.PI / 2) {
244            alpha = beta;
245            mThumbX = (int)((mRadius * Math.cos(alpha)) + (getWidth() / 2));
246            mThumbY = (int)((getHeight() / 2) - (mRadius * Math.sin(alpha)));
247        } else if (beta > Math.PI / 2 && beta < (Math.PI / 2) + MAX_ANGLE) {
248            alpha = beta - Math.PI;
249            mThumbX = (int)((getWidth() / 2) - (mRadius * Math.cos(alpha)));
250            mThumbY = (int)((getHeight() / 2) + (mRadius * Math.sin(alpha)));
251        } else if (beta <= 2 * Math.PI && beta > (3 * Math.PI) / 2) {
252            alpha = beta - (2 * Math.PI);
253            mThumbX = (int)((mRadius * Math.cos(alpha)) + (getWidth() / 2));
254            mThumbY = (int)((getHeight() / 2) - (mRadius * Math.sin(alpha)));
255        } else {
256            alpha = beta - Math.PI;
257            mThumbX = (int)((getWidth() / 2) - (mRadius * Math.cos(alpha)));
258            mThumbY = (int)((getHeight() / 2) + (mRadius * Math.sin(alpha)));
259        }
260    }
261
262    /**
263     * Compute the radians interval between progress values
264     */
265    private void computeInterval() {
266        mInterval = (Math.PI - MAX_ANGLE) / (mMaxProgress / 2);
267    }
268}
269