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.camera.ui;
18
19import static com.android.camera.ui.GLRootView.dpToPixel;
20import android.content.Context;
21import android.graphics.Color;
22import android.graphics.Rect;
23import android.view.MotionEvent;
24
25import com.android.camera.R;
26import com.android.camera.Util;
27
28import java.text.DecimalFormat;
29import java.util.Arrays;
30
31import javax.microedition.khronos.opengles.GL11;
32
33public class ZoomController extends GLView {
34    private static final int LABEL_COLOR = Color.WHITE;
35
36    private static final DecimalFormat sZoomFormat = new DecimalFormat("#.#x");
37    private static final int INVALID_POSITION = Integer.MAX_VALUE;
38
39    private static final float LABEL_FONT_SIZE = 18;
40    private static final int HORIZONTAL_PADDING = 3;
41    private static final int VERTICAL_PADDING = 3;
42    private static final int MINIMAL_HEIGHT = 150;
43    private static final float TOLERANCE_RADIUS = 30;
44
45    private static float sLabelSize;
46    private static int sHorizontalPadding;
47    private static int sVerticalPadding;
48    private static int sMinimalHeight;
49    private static float sToleranceRadius;
50
51    private static NinePatchTexture sBackground;
52    private static Texture sSlider;
53    private static Texture sTickMark;
54    private static Texture sFineTickMark;
55
56    private StringTexture mTickLabels[];
57    private float mRatios[];
58    private int mIndex;
59
60    private int mFineTickStep;
61    private int mLabelStep;
62
63    private int mMaxLabelWidth;
64    private int mMaxLabelHeight;
65
66    private int mSliderTop;
67    private int mSliderBottom;
68    private int mSliderLeft;
69    private int mSliderPosition = INVALID_POSITION;
70    private float mValueGap;
71    private ZoomListener mZoomListener;
72
73    public interface ZoomListener {
74        public void onZoomChanged(int index, float ratio, boolean isMoving);
75    }
76
77    public ZoomController(Context context) {
78        initializeStaticVariable(context);
79    }
80
81    private void onSliderMoved(int position, boolean isMoving) {
82        position = Util.clamp(position,
83                mSliderTop, mSliderBottom - sSlider.getHeight());
84        mSliderPosition = position;
85        invalidate();
86
87        int index = mRatios.length - 1 - (int)
88                ((position - mSliderTop) /  mValueGap + .5f);
89        if (index != mIndex || !isMoving) {
90            mIndex = index;
91            if (mZoomListener != null) {
92                mZoomListener.onZoomChanged(mIndex, mRatios[mIndex], isMoving);
93            }
94        }
95    }
96
97    private static void initializeStaticVariable(Context context) {
98        if (sBackground != null) return;
99
100        sLabelSize = dpToPixel(context, LABEL_FONT_SIZE);
101        sHorizontalPadding = dpToPixel(context, HORIZONTAL_PADDING);
102        sVerticalPadding = dpToPixel(context, VERTICAL_PADDING);
103        sMinimalHeight = dpToPixel(context, MINIMAL_HEIGHT);
104        sToleranceRadius = dpToPixel(context, TOLERANCE_RADIUS);
105
106        sBackground = new NinePatchTexture(context, R.drawable.zoom_background);
107        sSlider = new ResourceTexture(context, R.drawable.zoom_slider);
108        sTickMark = new ResourceTexture(context, R.drawable.zoom_tickmark);
109        sFineTickMark = new ResourceTexture(
110                context, R.drawable.zoom_finetickmark);
111    }
112
113    @Override
114    protected void onLayout(boolean changed, int l, int t, int r, int b) {
115        if (!changed) return;
116        Rect p = mPaddings;
117        int height = b - t - p.top - p.bottom;
118        int margin = Math.max(sSlider.getHeight(), mMaxLabelHeight);
119        mValueGap = (float) (height - margin) / (mRatios.length - 1);
120
121        mSliderLeft = p.left + mMaxLabelWidth + sHorizontalPadding
122                + sTickMark.getWidth() + sHorizontalPadding;
123
124        mSliderTop = p.top + margin / 2 - sSlider.getHeight() / 2;
125        mSliderBottom = mSliderTop + height - margin + sSlider.getHeight();
126    }
127
128    private boolean withInToleranceRange(float x, float y) {
129        float sx = mSliderLeft + sSlider.getWidth() / 2;
130        float sy = mSliderTop + (mRatios.length - 1 - mIndex) * mValueGap
131                + sSlider.getHeight() / 2;
132        float dist = Util.distance(x, y, sx, sy);
133        return dist <= sToleranceRadius;
134    }
135
136    @Override
137    protected boolean onTouch(MotionEvent e) {
138        float x = e.getX();
139        float y = e.getY();
140        switch (e.getAction()) {
141            case MotionEvent.ACTION_DOWN:
142                if (withInToleranceRange(x, y)) {
143                    onSliderMoved((int) (y - sSlider.getHeight()), true);
144                }
145                return true;
146            case MotionEvent.ACTION_MOVE:
147                if (mSliderPosition != INVALID_POSITION) {
148                    onSliderMoved((int) (y - sSlider.getHeight()), true);
149                }
150                return true;
151            case MotionEvent.ACTION_UP:
152                if (mSliderPosition != INVALID_POSITION) {
153                    onSliderMoved((int) (y - sSlider.getHeight()), false);
154                    mSliderPosition = INVALID_POSITION;
155                }
156                return true;
157        }
158        return true;
159    }
160
161    public void setAvailableZoomRatios(float ratios[]) {
162        if (Arrays.equals(ratios, mRatios)) return;
163        mRatios = ratios;
164        mLabelStep = getLabelStep(ratios.length);
165        mTickLabels = new StringTexture[
166                (ratios.length + mLabelStep - 1) / mLabelStep];
167        for (int i = 0, n = mTickLabels.length; i < n; ++i) {
168            mTickLabels[i] = StringTexture.newInstance(
169                    sZoomFormat.format(ratios[i * mLabelStep]),
170                    sLabelSize, LABEL_COLOR);
171        }
172        mFineTickStep = mLabelStep % 3 == 0
173                ? mLabelStep / 3
174                : mLabelStep %2 == 0 ? mLabelStep / 2 : 0;
175
176        int maxHeight = 0;
177        int maxWidth = 0;
178        int labelCount = mTickLabels.length;
179        for (int i = 0; i < labelCount; ++i) {
180            maxWidth = Math.max(maxWidth, mTickLabels[i].getWidth());
181            maxHeight = Math.max(maxHeight, mTickLabels[i].getHeight());
182        }
183
184        mMaxLabelHeight = maxHeight;
185        mMaxLabelWidth = maxWidth;
186        invalidate();
187    }
188
189    private int getLabelStep(final int valueCount) {
190        if (valueCount < 5) return 1;
191        for (int step = valueCount / 5;; ++step) {
192            if (valueCount / step <= 5) return step;
193        }
194    }
195
196    @Override
197    protected void onMeasure(int widthSpec, int heightSpec) {
198        int labelCount = mTickLabels.length;
199        int ratioCount = mRatios.length;
200
201        int height = (mMaxLabelHeight + sVerticalPadding)
202                * (labelCount - 1) * ratioCount / (mLabelStep * labelCount)
203                + Math.max(sSlider.getHeight(), mMaxLabelHeight);
204
205        int width = mMaxLabelWidth + sHorizontalPadding + sTickMark.getWidth()
206                + sHorizontalPadding + sBackground.getIntrinsicWidth();
207        height = Math.max(sMinimalHeight, height);
208
209        new MeasureHelper(this)
210                .setPreferredContentSize(width, height)
211                .measure(widthSpec, heightSpec);
212    }
213
214    @Override
215    protected void render(GLRootView root, GL11 gl) {
216        renderTicks(root, gl);
217        renderSlider(root, gl);
218    }
219
220    private void renderTicks(GLRootView root, GL11 gl) {
221        float gap = mValueGap;
222        int labelStep = mLabelStep;
223
224        // render the tick labels
225        int xoffset = mPaddings.left + mMaxLabelWidth;
226        float yoffset = mSliderBottom - sSlider.getHeight() / 2;
227        for (int i = 0, n = mTickLabels.length; i < n; ++i) {
228            Texture t = mTickLabels[i];
229            t.draw(root, xoffset - t.getWidth(),
230                    (int) (yoffset - t.getHeight() / 2));
231            yoffset -= labelStep * gap;
232        }
233
234        // render the main tick marks
235        Texture tickMark = sTickMark;
236        xoffset += sHorizontalPadding;
237        yoffset = mSliderBottom - sSlider.getHeight() / 2;
238        int halfHeight = tickMark.getHeight() / 2;
239        for (int i = 0, n = mTickLabels.length; i < n; ++i) {
240            tickMark.draw(root, xoffset, (int) (yoffset - halfHeight));
241            yoffset -= labelStep * gap;
242        }
243
244        if (mFineTickStep > 0) {
245            // render the fine tick marks
246            tickMark = sFineTickMark;
247            xoffset += sTickMark.getWidth() - tickMark.getWidth();
248            yoffset = mSliderBottom - sSlider.getHeight() / 2;
249            halfHeight = tickMark.getHeight() / 2;
250            for (int i = 0, n = mRatios.length; i < n; ++i) {
251                if (i % mLabelStep != 0) {
252                    tickMark.draw(root, xoffset, (int) (yoffset - halfHeight));
253                }
254                yoffset -= gap;
255            }
256        }
257    }
258
259    private void renderSlider(GLRootView root, GL11 gl) {
260        int left = mSliderLeft;
261        int bottom = mSliderBottom;
262        int top = mSliderTop;
263        sBackground.setSize(sBackground.getIntrinsicWidth(), bottom - top);
264        sBackground.draw(root, left, top);
265
266        if (mSliderPosition == INVALID_POSITION) {
267            sSlider.draw(root, left, (int)
268                    (top + mValueGap * (mRatios.length - 1 - mIndex)));
269        } else {
270            sSlider.draw(root, left, mSliderPosition);
271        }
272    }
273
274    public void setZoomListener(ZoomListener listener) {
275        mZoomListener = listener;
276    }
277
278    public void setZoomIndex(int index) {
279        index = Util.clamp(index, 0, mRatios.length - 1);
280        if (mIndex == index) return;
281        mIndex = index;
282        if (mZoomListener != null) {
283            mZoomListener.onZoomChanged(mIndex, mRatios[mIndex], false);
284        }
285    }
286}
287