blob: 940a2e5014cec0bbc048fa6e90c2065587b28152 [file] [log] [blame]
Jim Miller24ccf3c2009-11-10 21:37:45 -08001/*
2 * Copyright (C) 2009 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.internal.widget;
18
19import android.content.Context;
Jim Miller1d0a1522009-11-18 01:35:42 -080020import android.content.res.Configuration;
Jim Miller24ccf3c2009-11-10 21:37:45 -080021import android.content.res.Resources;
22import android.content.res.TypedArray;
Jim Miller521d4002009-11-15 16:19:24 -080023import android.graphics.Canvas;
Jim Miller24ccf3c2009-11-10 21:37:45 -080024import android.graphics.Rect;
Jim Miller753401a2009-11-11 17:53:53 -080025import android.graphics.drawable.Drawable;
Jim Miller24ccf3c2009-11-10 21:37:45 -080026import android.os.Handler;
27import android.os.Message;
28import android.os.Vibrator;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.view.Gravity;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.ViewGroup;
Jim Miller521d4002009-11-15 16:19:24 -080035import android.view.animation.AlphaAnimation;
36import android.view.animation.Animation;
37import android.view.animation.AnimationSet;
38import android.view.animation.LinearInterpolator;
39import android.view.animation.TranslateAnimation;
40import android.view.animation.Animation.AnimationListener;
Jim Miller24ccf3c2009-11-10 21:37:45 -080041import android.widget.ImageView;
42import android.widget.TextView;
43import android.widget.ImageView.ScaleType;
44import com.android.internal.R;
45
46/**
47 * A special widget containing two Sliders and a threshold for each. Moving either slider beyond
Jim Miller425ca592009-11-13 19:20:28 -080048 * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
Jim Miller4df2c542009-11-12 03:39:14 -080049 * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
Jim Miller425ca592009-11-13 19:20:28 -080050 * Equivalently, selecting a tab will result in a call to
Jim Miller4df2c542009-11-12 03:39:14 -080051 * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
52 * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
Jim Miller24ccf3c2009-11-10 21:37:45 -080053 *
54 */
55public class SlidingTab extends ViewGroup {
56 private static final String LOG_TAG = "SlidingTab";
57 private static final boolean DBG = false;
58 private static final int HORIZONTAL = 0; // as defined in attrs.xml
59 private static final int VERTICAL = 1;
Jim Miller24ccf3c2009-11-10 21:37:45 -080060
61 // TODO: Make these configurable
Jim Miller4df2c542009-11-12 03:39:14 -080062 private static final float THRESHOLD = 2.0f / 3.0f;
Jim Miller24ccf3c2009-11-10 21:37:45 -080063 private static final long VIBRATE_SHORT = 30;
64 private static final long VIBRATE_LONG = 40;
Jim Miller521d4002009-11-15 16:19:24 -080065 private static final int TRACKING_MARGIN = 50;
66 private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
67 private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
Jim Miller4f01d4a2009-11-16 23:08:35 -080068 private boolean mHoldLeftOnTransition = true;
69 private boolean mHoldRightOnTransition = true;
Jim Miller24ccf3c2009-11-10 21:37:45 -080070
71 private OnTriggerListener mOnTriggerListener;
72 private int mGrabbedState = OnTriggerListener.NO_HANDLE;
73 private boolean mTriggered = false;
74 private Vibrator mVibrator;
75 private float mDensity; // used to scale dimensions for bitmaps.
76
Jim Miller24ccf3c2009-11-10 21:37:45 -080077 /**
78 * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
79 */
80 private int mOrientation;
81
82 private Slider mLeftSlider;
83 private Slider mRightSlider;
84 private Slider mCurrentSlider;
85 private boolean mTracking;
Jim Miller4df2c542009-11-12 03:39:14 -080086 private float mThreshold;
Jim Miller24ccf3c2009-11-10 21:37:45 -080087 private Slider mOtherSlider;
88 private boolean mAnimating;
Jim Miller521d4002009-11-15 16:19:24 -080089 private Rect mTmpRect;
Jim Miller24ccf3c2009-11-10 21:37:45 -080090
91 /**
Jim Miller4f01d4a2009-11-16 23:08:35 -080092 * Listener used to reset the view when the current animation completes.
93 */
94 private final AnimationListener mAnimationDoneListener = new AnimationListener() {
95 public void onAnimationStart(Animation animation) {
96
97 }
98
99 public void onAnimationRepeat(Animation animation) {
100
101 }
102
103 public void onAnimationEnd(Animation animation) {
104 onAnimationDone();
105 }
106 };
107
108 /**
Jim Miller24ccf3c2009-11-10 21:37:45 -0800109 * Interface definition for a callback to be invoked when a tab is triggered
Jim Miller4df2c542009-11-12 03:39:14 -0800110 * by moving it beyond a threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800111 */
112 public interface OnTriggerListener {
113 /**
114 * The interface was triggered because the user let go of the handle without reaching the
Jim Miller4df2c542009-11-12 03:39:14 -0800115 * threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800116 */
117 public static final int NO_HANDLE = 0;
118
119 /**
120 * The interface was triggered because the user grabbed the left handle and moved it past
Jim Miller4df2c542009-11-12 03:39:14 -0800121 * the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800122 */
123 public static final int LEFT_HANDLE = 1;
124
125 /**
126 * The interface was triggered because the user grabbed the right handle and moved it past
Jim Miller4df2c542009-11-12 03:39:14 -0800127 * the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800128 */
129 public static final int RIGHT_HANDLE = 2;
130
131 /**
Jim Miller4df2c542009-11-12 03:39:14 -0800132 * Called when the user moves a handle beyond the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800133 *
134 * @param v The view that was triggered.
135 * @param whichHandle Which "dial handle" the user grabbed,
136 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
137 */
138 void onTrigger(View v, int whichHandle);
139
140 /**
141 * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
142 * one of the handles.)
143 *
144 * @param v the view that was triggered
145 * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
146 * or {@link #RIGHT_HANDLE}.
147 */
148 void onGrabbedStateChange(View v, int grabbedState);
149 }
150
151 /**
Jim Miller425ca592009-11-13 19:20:28 -0800152 * Simple container class for all things pertinent to a slider.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800153 * A slider consists of 3 Views:
Jim Miller425ca592009-11-13 19:20:28 -0800154 *
Jim Miller24ccf3c2009-11-10 21:37:45 -0800155 * {@link #tab} is the tab shown on the screen in the default state.
156 * {@link #text} is the view revealed as the user slides the tab out.
157 * {@link #target} is the target the user must drag the slider past to trigger the slider.
158 *
159 */
160 private static class Slider {
161 /**
162 * Tab alignment - determines which side the tab should be drawn on
163 */
164 public static final int ALIGN_LEFT = 0;
165 public static final int ALIGN_RIGHT = 1;
166 public static final int ALIGN_TOP = 2;
167 public static final int ALIGN_BOTTOM = 3;
Jim Miller521d4002009-11-15 16:19:24 -0800168 public static final int ALIGN_UNKNOWN = 4;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800169
170 /**
171 * States for the view.
172 */
173 private static final int STATE_NORMAL = 0;
174 private static final int STATE_PRESSED = 1;
175 private static final int STATE_ACTIVE = 2;
176
177 private final ImageView tab;
178 private final TextView text;
179 private final ImageView target;
Jim Miller4df2c542009-11-12 03:39:14 -0800180 private int currentState = STATE_NORMAL;
Jim Miller521d4002009-11-15 16:19:24 -0800181 private int alignment = ALIGN_UNKNOWN;
182 private int alignment_value;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800183
184 /**
185 * Constructor
Jim Miller425ca592009-11-13 19:20:28 -0800186 *
Jim Miller24ccf3c2009-11-10 21:37:45 -0800187 * @param parent the container view of this one
188 * @param tabId drawable for the tab
189 * @param barId drawable for the bar
190 * @param targetId drawable for the target
191 */
192 Slider(ViewGroup parent, int tabId, int barId, int targetId) {
193 // Create tab
194 tab = new ImageView(parent.getContext());
195 tab.setBackgroundResource(tabId);
196 tab.setScaleType(ScaleType.CENTER);
197 tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
198 LayoutParams.WRAP_CONTENT));
199
200 // Create hint TextView
201 text = new TextView(parent.getContext());
202 text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
203 LayoutParams.FILL_PARENT));
204 text.setBackgroundResource(barId);
205 text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
206 // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen
207
208 // Create target
209 target = new ImageView(parent.getContext());
210 target.setImageResource(targetId);
211 target.setScaleType(ScaleType.CENTER);
212 target.setLayoutParams(
213 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
214 target.setVisibility(View.INVISIBLE);
215
216 parent.addView(target); // this needs to be first - relies on painter's algorithm
217 parent.addView(tab);
218 parent.addView(text);
219 }
220
221 void setIcon(int iconId) {
222 tab.setImageResource(iconId);
223 }
Jim Miller425ca592009-11-13 19:20:28 -0800224
Jim Miller24ccf3c2009-11-10 21:37:45 -0800225 void setTabBackgroundResource(int tabId) {
226 tab.setBackgroundResource(tabId);
227 }
Jim Miller425ca592009-11-13 19:20:28 -0800228
Jim Miller24ccf3c2009-11-10 21:37:45 -0800229 void setBarBackgroundResource(int barId) {
230 text.setBackgroundResource(barId);
231 }
Jim Miller425ca592009-11-13 19:20:28 -0800232
Jim Miller24ccf3c2009-11-10 21:37:45 -0800233 void setHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800234 text.setText(resId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800235 }
236
237 void hide() {
Jim Miller521d4002009-11-15 16:19:24 -0800238 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
239 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
240 : alignment_value - tab.getLeft()) : 0;
241 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
242 : alignment_value - tab.getTop());
243
244 Animation trans = new TranslateAnimation(0, dx, 0, dy);
245 trans.setDuration(ANIM_DURATION);
246 trans.setFillAfter(true);
247 tab.startAnimation(trans);
248 text.startAnimation(trans);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800249 target.setVisibility(View.INVISIBLE);
250 }
251
Jim Miller521d4002009-11-15 16:19:24 -0800252 void show(boolean animate) {
253 text.setVisibility(View.VISIBLE);
254 tab.setVisibility(View.VISIBLE);
255 //target.setVisibility(View.INVISIBLE);
256 if (animate) {
257 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
258 int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
259 int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
260
261 Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
262 trans.setDuration(ANIM_DURATION);
263 tab.startAnimation(trans);
264 text.startAnimation(trans);
265 }
266 }
267
Jim Miller24ccf3c2009-11-10 21:37:45 -0800268 void setState(int state) {
269 text.setPressed(state == STATE_PRESSED);
270 tab.setPressed(state == STATE_PRESSED);
271 if (state == STATE_ACTIVE) {
272 final int[] activeState = new int[] {com.android.internal.R.attr.state_active};
273 if (text.getBackground().isStateful()) {
274 text.getBackground().setState(activeState);
275 }
276 if (tab.getBackground().isStateful()) {
277 tab.getBackground().setState(activeState);
278 }
279 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
280 } else {
281 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
282 }
Jim Miller4df2c542009-11-12 03:39:14 -0800283 currentState = state;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800284 }
285
286 void showTarget() {
Jim Miller521d4002009-11-15 16:19:24 -0800287 AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
288 alphaAnim.setDuration(ANIM_TARGET_TIME);
289 target.startAnimation(alphaAnim);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800290 target.setVisibility(View.VISIBLE);
291 }
292
Jim Miller521d4002009-11-15 16:19:24 -0800293 void reset(boolean animate) {
Jim Miller24ccf3c2009-11-10 21:37:45 -0800294 setState(STATE_NORMAL);
295 text.setVisibility(View.VISIBLE);
296 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
297 tab.setVisibility(View.VISIBLE);
298 target.setVisibility(View.INVISIBLE);
Jim Miller521d4002009-11-15 16:19:24 -0800299 final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
300 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getLeft()
301 : alignment_value - tab.getRight()) : 0;
302 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
303 : alignment_value - tab.getBottom());
304 if (animate) {
305 TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
306 trans.setDuration(ANIM_DURATION);
307 trans.setFillAfter(false);
308 text.startAnimation(trans);
309 tab.startAnimation(trans);
310 } else {
311 if (horiz) {
312 text.offsetLeftAndRight(dx);
313 tab.offsetLeftAndRight(dx);
314 } else {
315 text.offsetTopAndBottom(dy);
316 tab.offsetTopAndBottom(dy);
317 }
Jim Miller4f01d4a2009-11-16 23:08:35 -0800318 text.clearAnimation();
319 tab.clearAnimation();
320 target.clearAnimation();
Jim Miller521d4002009-11-15 16:19:24 -0800321 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800322 }
323
324 void setTarget(int targetId) {
325 target.setImageResource(targetId);
326 }
327
328 /**
329 * Layout the given widgets within the parent.
330 *
331 * @param l the parent's left border
332 * @param t the parent's top border
333 * @param r the parent's right border
334 * @param b the parent's bottom border
335 * @param alignment which side to align the widget to
336 */
337 void layout(int l, int t, int r, int b, int alignment) {
Jim Miller521d4002009-11-15 16:19:24 -0800338 this.alignment = alignment;
Jim Miller753401a2009-11-11 17:53:53 -0800339 final Drawable tabBackground = tab.getBackground();
340 final int handleWidth = tabBackground.getIntrinsicWidth();
341 final int handleHeight = tabBackground.getIntrinsicHeight();
342 final Drawable targetDrawable = target.getDrawable();
343 final int targetWidth = targetDrawable.getIntrinsicWidth();
344 final int targetHeight = targetDrawable.getIntrinsicHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800345 final int parentWidth = r - l;
346 final int parentHeight = b - t;
347
Jim Miller4df2c542009-11-12 03:39:14 -0800348 final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
349 final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800350 final int left = (parentWidth - handleWidth) / 2;
351 final int right = left + handleWidth;
352
353 if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
354 // horizontal
355 final int targetTop = (parentHeight - targetHeight) / 2;
356 final int targetBottom = targetTop + targetHeight;
357 final int top = (parentHeight - handleHeight) / 2;
358 final int bottom = (parentHeight + handleHeight) / 2;
359 if (alignment == ALIGN_LEFT) {
360 tab.layout(0, top, handleWidth, bottom);
361 text.layout(0 - parentWidth, top, 0, bottom);
362 text.setGravity(Gravity.RIGHT);
363 target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
Jim Miller521d4002009-11-15 16:19:24 -0800364 alignment_value = l;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800365 } else {
366 tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
367 text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
368 target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
369 text.setGravity(Gravity.TOP);
Jim Miller521d4002009-11-15 16:19:24 -0800370 alignment_value = r;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800371 }
372 } else {
373 // vertical
374 final int targetLeft = (parentWidth - targetWidth) / 2;
375 final int targetRight = (parentWidth + targetWidth) / 2;
Jim Miller4df2c542009-11-12 03:39:14 -0800376 final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
377 final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800378 if (alignment == ALIGN_TOP) {
379 tab.layout(left, 0, right, handleHeight);
380 text.layout(left, 0 - parentHeight, right, 0);
381 target.layout(targetLeft, top, targetRight, top + targetHeight);
Jim Miller521d4002009-11-15 16:19:24 -0800382 alignment_value = t;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800383 } else {
384 tab.layout(left, parentHeight - handleHeight, right, parentHeight);
385 text.layout(left, parentHeight, right, parentHeight + parentHeight);
386 target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
Jim Miller521d4002009-11-15 16:19:24 -0800387 alignment_value = b;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800388 }
389 }
390 }
391
Jim Miller4df2c542009-11-12 03:39:14 -0800392 public void updateDrawableStates() {
393 setState(currentState);
394 }
395
Jim Miller4811d622009-11-12 17:45:07 -0800396 /**
397 * Ensure all the dependent widgets are measured.
398 */
399 public void measure() {
400 tab.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
401 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
402 text.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
403 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
Jim Miller24ccf3c2009-11-10 21:37:45 -0800404 }
405
Jim Miller4811d622009-11-12 17:45:07 -0800406 /**
407 * Get the measured tab width. Must be called after {@link Slider#measure()}.
408 * @return
409 */
410 public int getTabWidth() {
411 return tab.getMeasuredWidth();
412 }
413
414 /**
415 * Get the measured tab width. Must be called after {@link Slider#measure()}.
416 * @return
417 */
Jim Miller24ccf3c2009-11-10 21:37:45 -0800418 public int getTabHeight() {
Jim Miller4811d622009-11-12 17:45:07 -0800419 return tab.getMeasuredHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800420 }
Jim Miller521d4002009-11-15 16:19:24 -0800421
Jim Miller8b63ab62009-12-02 13:42:48 -0800422 /**
423 * Start animating the slider. Note we need two animations since an Animator
424 * keeps internal state of the invalidation region which is just the view being animated.
Jim Miller2cd1e6e2010-01-15 16:20:36 -0800425 *
Jim Miller8b63ab62009-12-02 13:42:48 -0800426 * @param anim1
427 * @param anim2
428 */
429 public void startAnimation(Animation anim1, Animation anim2) {
430 tab.startAnimation(anim1);
431 text.startAnimation(anim2);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800432 }
433
434 public void hideTarget() {
435 target.clearAnimation();
436 target.setVisibility(View.INVISIBLE);
Jim Miller521d4002009-11-15 16:19:24 -0800437 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800438 }
439
440 public SlidingTab(Context context) {
441 this(context, null);
442 }
443
444 /**
445 * Constructor used when this widget is created from a layout file.
446 */
447 public SlidingTab(Context context, AttributeSet attrs) {
448 super(context, attrs);
449
Jim Miller521d4002009-11-15 16:19:24 -0800450 // Allocate a temporary once that can be used everywhere.
451 mTmpRect = new Rect();
452
Jim Miller24ccf3c2009-11-10 21:37:45 -0800453 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
454 mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
455 a.recycle();
456
457 Resources r = getResources();
458 mDensity = r.getDisplayMetrics().density;
459 if (DBG) log("- Density: " + mDensity);
460
Jim Miller425ca592009-11-13 19:20:28 -0800461 mLeftSlider = new Slider(this,
462 R.drawable.jog_tab_left_generic,
Jim Miller24ccf3c2009-11-10 21:37:45 -0800463 R.drawable.jog_tab_bar_left_generic,
464 R.drawable.jog_tab_target_gray);
Jim Miller425ca592009-11-13 19:20:28 -0800465 mRightSlider = new Slider(this,
466 R.drawable.jog_tab_right_generic,
Jim Miller24ccf3c2009-11-10 21:37:45 -0800467 R.drawable.jog_tab_bar_right_generic,
468 R.drawable.jog_tab_target_gray);
469
470 // setBackgroundColor(0x80808080);
471 }
472
473 @Override
474 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
475 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
476 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
477
478 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
479 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
480
481 if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
482 throw new RuntimeException(LOG_TAG + " cannot have UNSPECIFIED dimensions");
483 }
484
Jim Miller4811d622009-11-12 17:45:07 -0800485 mLeftSlider.measure();
486 mRightSlider.measure();
487 final int leftTabWidth = mLeftSlider.getTabWidth();
488 final int rightTabWidth = mRightSlider.getTabWidth();
489 final int leftTabHeight = mLeftSlider.getTabHeight();
490 final int rightTabHeight = mRightSlider.getTabHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800491 final int width;
492 final int height;
493 if (isHorizontal()) {
494 width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
495 height = Math.max(leftTabHeight, rightTabHeight);
496 } else {
497 width = Math.max(leftTabWidth, rightTabHeight);
498 height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
499 }
500 setMeasuredDimension(width, height);
501 }
502
503 @Override
504 public boolean onInterceptTouchEvent(MotionEvent event) {
505 final int action = event.getAction();
506 final float x = event.getX();
507 final float y = event.getY();
508
Jim Miller24ccf3c2009-11-10 21:37:45 -0800509 if (mAnimating) {
510 return false;
511 }
512
513 View leftHandle = mLeftSlider.tab;
Jim Miller521d4002009-11-15 16:19:24 -0800514 leftHandle.getHitRect(mTmpRect);
515 boolean leftHit = mTmpRect.contains((int) x, (int) y);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800516
517 View rightHandle = mRightSlider.tab;
Jim Miller521d4002009-11-15 16:19:24 -0800518 rightHandle.getHitRect(mTmpRect);
519 boolean rightHit = mTmpRect.contains((int)x, (int) y);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800520
521 if (!mTracking && !(leftHit || rightHit)) {
522 return false;
523 }
524
525 switch (action) {
526 case MotionEvent.ACTION_DOWN: {
527 mTracking = true;
528 mTriggered = false;
529 vibrate(VIBRATE_SHORT);
530 if (leftHit) {
531 mCurrentSlider = mLeftSlider;
532 mOtherSlider = mRightSlider;
Jim Miller4df2c542009-11-12 03:39:14 -0800533 mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800534 setGrabbedState(OnTriggerListener.LEFT_HANDLE);
535 } else {
536 mCurrentSlider = mRightSlider;
537 mOtherSlider = mLeftSlider;
Jim Miller4df2c542009-11-12 03:39:14 -0800538 mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800539 setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
540 }
541 mCurrentSlider.setState(Slider.STATE_PRESSED);
542 mCurrentSlider.showTarget();
543 mOtherSlider.hide();
544 break;
545 }
546 }
547
548 return true;
549 }
550
551 @Override
Jim Miller4f01d4a2009-11-16 23:08:35 -0800552 public void setVisibility(int visibility) {
553 // Clear animations so sliders don't continue to animate when we show the widget again.
554 if (visibility != getVisibility() && visibility == View.INVISIBLE) {
555 mLeftSlider.reset(false);
556 mRightSlider.reset(false);
557 }
558 super.setVisibility(visibility);
559 }
560
561 @Override
Jim Miller24ccf3c2009-11-10 21:37:45 -0800562 public boolean onTouchEvent(MotionEvent event) {
563 if (mTracking) {
564 final int action = event.getAction();
565 final float x = event.getX();
566 final float y = event.getY();
Jim Miller521d4002009-11-15 16:19:24 -0800567
Jim Miller24ccf3c2009-11-10 21:37:45 -0800568 switch (action) {
569 case MotionEvent.ACTION_MOVE:
Jim Miller521d4002009-11-15 16:19:24 -0800570 if (withinView(x, y, this) ) {
571 moveHandle(x, y);
572 float position = isHorizontal() ? x : y;
573 float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
574 boolean thresholdReached;
575 if (isHorizontal()) {
576 thresholdReached = mCurrentSlider == mLeftSlider ?
577 position > target : position < target;
578 } else {
579 thresholdReached = mCurrentSlider == mLeftSlider ?
580 position < target : position > target;
581 }
582 if (!mTriggered && thresholdReached) {
583 mTriggered = true;
584 mTracking = false;
585 mCurrentSlider.setState(Slider.STATE_ACTIVE);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800586 boolean isLeft = mCurrentSlider == mLeftSlider;
587 dispatchTriggerEvent(isLeft ?
Jim Miller521d4002009-11-15 16:19:24 -0800588 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800589
Jim Miller4f01d4a2009-11-16 23:08:35 -0800590 startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
Jim Miller521d4002009-11-15 16:19:24 -0800591 setGrabbedState(OnTriggerListener.NO_HANDLE);
592 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800593 break;
594 }
595 // Intentionally fall through - we're outside tracking rectangle
596
597 case MotionEvent.ACTION_UP:
598 case MotionEvent.ACTION_CANCEL:
599 mTracking = false;
600 mTriggered = false;
Jim Miller521d4002009-11-15 16:19:24 -0800601 mOtherSlider.show(true);
602 mCurrentSlider.reset(false);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800603 mCurrentSlider.hideTarget();
Jim Miller521d4002009-11-15 16:19:24 -0800604 mCurrentSlider = null;
605 mOtherSlider = null;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800606 setGrabbedState(OnTriggerListener.NO_HANDLE);
607 break;
608 }
609 }
610
611 return mTracking || super.onTouchEvent(event);
612 }
613
Jim Miller4f01d4a2009-11-16 23:08:35 -0800614 void startAnimating(final boolean holdAfter) {
Jim Miller521d4002009-11-15 16:19:24 -0800615 mAnimating = true;
Jim Miller8b63ab62009-12-02 13:42:48 -0800616 final Animation trans1;
617 final Animation trans2;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800618 final Slider slider = mCurrentSlider;
619 final Slider other = mOtherSlider;
620 final int dx;
621 final int dy;
Jim Miller521d4002009-11-15 16:19:24 -0800622 if (isHorizontal()) {
623 int right = slider.tab.getRight();
624 int width = slider.tab.getWidth();
625 int left = slider.tab.getLeft();
626 int viewWidth = getWidth();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800627 int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
628 dx = slider == mRightSlider ? - (right + viewWidth - holdOffset)
629 : (viewWidth - left) + viewWidth - holdOffset;
Jim Miller521d4002009-11-15 16:19:24 -0800630 dy = 0;
631 } else {
632 int top = slider.tab.getTop();
633 int bottom = slider.tab.getBottom();
634 int height = slider.tab.getHeight();
635 int viewHeight = getHeight();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800636 int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
Jim Miller521d4002009-11-15 16:19:24 -0800637 dx = 0;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800638 dy = slider == mRightSlider ? (top + viewHeight - holdOffset)
639 : - ((viewHeight - bottom) + viewHeight - holdOffset);
Jim Miller521d4002009-11-15 16:19:24 -0800640 }
Jim Miller8b63ab62009-12-02 13:42:48 -0800641 trans1 = new TranslateAnimation(0, dx, 0, dy);
642 trans1.setDuration(ANIM_DURATION);
643 trans1.setInterpolator(new LinearInterpolator());
644 trans1.setFillAfter(true);
645 trans2 = new TranslateAnimation(0, dx, 0, dy);
646 trans2.setDuration(ANIM_DURATION);
647 trans2.setInterpolator(new LinearInterpolator());
648 trans2.setFillAfter(true);
Jim Miller521d4002009-11-15 16:19:24 -0800649
Jim Miller8b63ab62009-12-02 13:42:48 -0800650 trans1.setAnimationListener(new AnimationListener() {
Jim Miller521d4002009-11-15 16:19:24 -0800651 public void onAnimationEnd(Animation animation) {
Jim Miller4f01d4a2009-11-16 23:08:35 -0800652 Animation anim;
653 if (holdAfter) {
654 anim = new TranslateAnimation(dx, dx, dy, dy);
655 anim.setDuration(1000); // plenty of time for transitions
656 mAnimating = false;
657 } else {
658 anim = new AlphaAnimation(0.5f, 1.0f);
659 anim.setDuration(ANIM_DURATION);
660 resetView();
661 }
662 anim.setAnimationListener(mAnimationDoneListener);
Jim Miller2cd1e6e2010-01-15 16:20:36 -0800663
Jim Miller8b63ab62009-12-02 13:42:48 -0800664 /* Animation can be the same for these since the animation just holds */
665 mLeftSlider.startAnimation(anim, anim);
666 mRightSlider.startAnimation(anim, anim);
Jim Miller521d4002009-11-15 16:19:24 -0800667 }
668
669 public void onAnimationRepeat(Animation animation) {
670
671 }
672
673 public void onAnimationStart(Animation animation) {
674
675 }
676
677 });
678
Jim Miller4f01d4a2009-11-16 23:08:35 -0800679 slider.hideTarget();
Jim Miller8b63ab62009-12-02 13:42:48 -0800680 slider.startAnimation(trans1, trans2);
Jim Miller521d4002009-11-15 16:19:24 -0800681 }
682
Jim Miller4f01d4a2009-11-16 23:08:35 -0800683 private void onAnimationDone() {
684 resetView();
685 mAnimating = false;
686 }
687
Jim Miller521d4002009-11-15 16:19:24 -0800688 private boolean withinView(final float x, final float y, final View view) {
689 return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
690 || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
691 }
692
Jim Miller24ccf3c2009-11-10 21:37:45 -0800693 private boolean isHorizontal() {
694 return mOrientation == HORIZONTAL;
695 }
696
697 private void resetView() {
Jim Miller521d4002009-11-15 16:19:24 -0800698 mLeftSlider.reset(false);
699 mRightSlider.reset(false);
700 // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
Jim Miller24ccf3c2009-11-10 21:37:45 -0800701 }
702
703 @Override
704 protected void onLayout(boolean changed, int l, int t, int r, int b) {
705 if (!changed) return;
706
707 // Center the widgets in the view
708 mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
709 mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800710 }
711
712 private void moveHandle(float x, float y) {
713 final View handle = mCurrentSlider.tab;
714 final View content = mCurrentSlider.text;
715 if (isHorizontal()) {
716 int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
717 handle.offsetLeftAndRight(deltaX);
718 content.offsetLeftAndRight(deltaX);
719 } else {
720 int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
721 handle.offsetTopAndBottom(deltaY);
722 content.offsetTopAndBottom(deltaY);
723 }
724 invalidate(); // TODO: be more conservative about what we're invalidating
725 }
726
727 /**
728 * Sets the left handle icon to a given resource.
729 *
730 * The resource should refer to a Drawable object, or use 0 to remove
731 * the icon.
732 *
733 * @param iconId the resource ID of the icon drawable
734 * @param targetId the resource of the target drawable
735 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800736 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800737 */
738 public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
739 mLeftSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800740 mLeftSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800741 mLeftSlider.setBarBackgroundResource(barId);
742 mLeftSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800743 mLeftSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800744 }
745
746 /**
747 * Sets the left handle hint text to a given resource string.
748 *
749 * @param resId
750 */
751 public void setLeftHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800752 if (isHorizontal()) {
753 mLeftSlider.setHintText(resId);
754 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800755 }
756
757 /**
758 * Sets the right handle icon to a given resource.
759 *
760 * The resource should refer to a Drawable object, or use 0 to remove
761 * the icon.
762 *
763 * @param iconId the resource ID of the icon drawable
764 * @param targetId the resource of the target drawable
765 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800766 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800767 */
768 public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
769 mRightSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800770 mRightSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800771 mRightSlider.setBarBackgroundResource(barId);
772 mRightSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800773 mRightSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800774 }
775
776 /**
777 * Sets the left handle hint text to a given resource string.
778 *
779 * @param resId
780 */
781 public void setRightHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800782 if (isHorizontal()) {
783 mRightSlider.setHintText(resId);
784 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800785 }
786
Jim Miller4f01d4a2009-11-16 23:08:35 -0800787 public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
788 mHoldLeftOnTransition = holdLeft;
789 mHoldRightOnTransition = holdRight;
790 }
791
Jim Miller24ccf3c2009-11-10 21:37:45 -0800792 /**
793 * Triggers haptic feedback.
794 */
795 private synchronized void vibrate(long duration) {
796 if (mVibrator == null) {
797 mVibrator = (android.os.Vibrator)
798 getContext().getSystemService(Context.VIBRATOR_SERVICE);
799 }
800 mVibrator.vibrate(duration);
801 }
802
803 /**
804 * Registers a callback to be invoked when the user triggers an event.
805 *
806 * @param listener the OnDialTriggerListener to attach to this view
807 */
808 public void setOnTriggerListener(OnTriggerListener listener) {
809 mOnTriggerListener = listener;
810 }
811
812 /**
813 * Dispatches a trigger event to listener. Ignored if a listener is not set.
814 * @param whichHandle the handle that triggered the event.
815 */
816 private void dispatchTriggerEvent(int whichHandle) {
817 vibrate(VIBRATE_LONG);
818 if (mOnTriggerListener != null) {
819 mOnTriggerListener.onTrigger(this, whichHandle);
820 }
821 }
822
823 /**
824 * Sets the current grabbed state, and dispatches a grabbed state change
825 * event to our listener.
826 */
827 private void setGrabbedState(int newState) {
828 if (newState != mGrabbedState) {
829 mGrabbedState = newState;
830 if (mOnTriggerListener != null) {
831 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
832 }
833 }
834 }
835
Jim Miller24ccf3c2009-11-10 21:37:45 -0800836 private void log(String msg) {
837 Log.d(LOG_TAG, msg);
838 }
839}