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