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