blob: 07955c4b9aaad35dc884ab1cf8106d6da78759c0 [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
Jim Millerd8a3a892010-01-29 17:25:26 -0800551 /**
552 * Reset the tabs to their original state and stop any existing animation.
553 * Animate them back into place if animate is true.
554 *
555 * @param animate
556 */
557 public void reset(boolean animate) {
558 mLeftSlider.reset(animate);
559 mRightSlider.reset(animate);
560 }
561
Jim Miller24ccf3c2009-11-10 21:37:45 -0800562 @Override
Jim Miller4f01d4a2009-11-16 23:08:35 -0800563 public void setVisibility(int visibility) {
564 // Clear animations so sliders don't continue to animate when we show the widget again.
565 if (visibility != getVisibility() && visibility == View.INVISIBLE) {
Jim Millerd8a3a892010-01-29 17:25:26 -0800566 reset(false);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800567 }
568 super.setVisibility(visibility);
569 }
570
571 @Override
Jim Miller24ccf3c2009-11-10 21:37:45 -0800572 public boolean onTouchEvent(MotionEvent event) {
573 if (mTracking) {
574 final int action = event.getAction();
575 final float x = event.getX();
576 final float y = event.getY();
Jim Miller521d4002009-11-15 16:19:24 -0800577
Jim Miller24ccf3c2009-11-10 21:37:45 -0800578 switch (action) {
579 case MotionEvent.ACTION_MOVE:
Jim Miller521d4002009-11-15 16:19:24 -0800580 if (withinView(x, y, this) ) {
581 moveHandle(x, y);
582 float position = isHorizontal() ? x : y;
583 float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
584 boolean thresholdReached;
585 if (isHorizontal()) {
586 thresholdReached = mCurrentSlider == mLeftSlider ?
587 position > target : position < target;
588 } else {
589 thresholdReached = mCurrentSlider == mLeftSlider ?
590 position < target : position > target;
591 }
592 if (!mTriggered && thresholdReached) {
593 mTriggered = true;
594 mTracking = false;
595 mCurrentSlider.setState(Slider.STATE_ACTIVE);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800596 boolean isLeft = mCurrentSlider == mLeftSlider;
597 dispatchTriggerEvent(isLeft ?
Jim Miller521d4002009-11-15 16:19:24 -0800598 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800599
Jim Miller4f01d4a2009-11-16 23:08:35 -0800600 startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
Jim Miller521d4002009-11-15 16:19:24 -0800601 setGrabbedState(OnTriggerListener.NO_HANDLE);
602 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800603 break;
604 }
605 // Intentionally fall through - we're outside tracking rectangle
606
607 case MotionEvent.ACTION_UP:
608 case MotionEvent.ACTION_CANCEL:
609 mTracking = false;
610 mTriggered = false;
Jim Miller521d4002009-11-15 16:19:24 -0800611 mOtherSlider.show(true);
612 mCurrentSlider.reset(false);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800613 mCurrentSlider.hideTarget();
Jim Miller521d4002009-11-15 16:19:24 -0800614 mCurrentSlider = null;
615 mOtherSlider = null;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800616 setGrabbedState(OnTriggerListener.NO_HANDLE);
617 break;
618 }
619 }
620
621 return mTracking || super.onTouchEvent(event);
622 }
623
Jim Miller4f01d4a2009-11-16 23:08:35 -0800624 void startAnimating(final boolean holdAfter) {
Jim Miller521d4002009-11-15 16:19:24 -0800625 mAnimating = true;
Jim Miller8b63ab62009-12-02 13:42:48 -0800626 final Animation trans1;
627 final Animation trans2;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800628 final Slider slider = mCurrentSlider;
629 final Slider other = mOtherSlider;
630 final int dx;
631 final int dy;
Jim Miller521d4002009-11-15 16:19:24 -0800632 if (isHorizontal()) {
633 int right = slider.tab.getRight();
634 int width = slider.tab.getWidth();
635 int left = slider.tab.getLeft();
636 int viewWidth = getWidth();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800637 int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
638 dx = slider == mRightSlider ? - (right + viewWidth - holdOffset)
639 : (viewWidth - left) + viewWidth - holdOffset;
Jim Miller521d4002009-11-15 16:19:24 -0800640 dy = 0;
641 } else {
642 int top = slider.tab.getTop();
643 int bottom = slider.tab.getBottom();
644 int height = slider.tab.getHeight();
645 int viewHeight = getHeight();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800646 int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
Jim Miller521d4002009-11-15 16:19:24 -0800647 dx = 0;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800648 dy = slider == mRightSlider ? (top + viewHeight - holdOffset)
649 : - ((viewHeight - bottom) + viewHeight - holdOffset);
Jim Miller521d4002009-11-15 16:19:24 -0800650 }
Jim Miller8b63ab62009-12-02 13:42:48 -0800651 trans1 = new TranslateAnimation(0, dx, 0, dy);
652 trans1.setDuration(ANIM_DURATION);
653 trans1.setInterpolator(new LinearInterpolator());
654 trans1.setFillAfter(true);
655 trans2 = new TranslateAnimation(0, dx, 0, dy);
656 trans2.setDuration(ANIM_DURATION);
657 trans2.setInterpolator(new LinearInterpolator());
658 trans2.setFillAfter(true);
Jim Miller521d4002009-11-15 16:19:24 -0800659
Jim Miller8b63ab62009-12-02 13:42:48 -0800660 trans1.setAnimationListener(new AnimationListener() {
Jim Miller521d4002009-11-15 16:19:24 -0800661 public void onAnimationEnd(Animation animation) {
Jim Miller4f01d4a2009-11-16 23:08:35 -0800662 Animation anim;
663 if (holdAfter) {
664 anim = new TranslateAnimation(dx, dx, dy, dy);
665 anim.setDuration(1000); // plenty of time for transitions
666 mAnimating = false;
667 } else {
668 anim = new AlphaAnimation(0.5f, 1.0f);
669 anim.setDuration(ANIM_DURATION);
670 resetView();
671 }
672 anim.setAnimationListener(mAnimationDoneListener);
Jim Miller2cd1e6e2010-01-15 16:20:36 -0800673
Jim Miller8b63ab62009-12-02 13:42:48 -0800674 /* Animation can be the same for these since the animation just holds */
675 mLeftSlider.startAnimation(anim, anim);
676 mRightSlider.startAnimation(anim, anim);
Jim Miller521d4002009-11-15 16:19:24 -0800677 }
678
679 public void onAnimationRepeat(Animation animation) {
680
681 }
682
683 public void onAnimationStart(Animation animation) {
684
685 }
686
687 });
688
Jim Miller4f01d4a2009-11-16 23:08:35 -0800689 slider.hideTarget();
Jim Miller8b63ab62009-12-02 13:42:48 -0800690 slider.startAnimation(trans1, trans2);
Jim Miller521d4002009-11-15 16:19:24 -0800691 }
692
Jim Miller4f01d4a2009-11-16 23:08:35 -0800693 private void onAnimationDone() {
694 resetView();
695 mAnimating = false;
696 }
697
Jim Miller521d4002009-11-15 16:19:24 -0800698 private boolean withinView(final float x, final float y, final View view) {
699 return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
700 || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
701 }
702
Jim Miller24ccf3c2009-11-10 21:37:45 -0800703 private boolean isHorizontal() {
704 return mOrientation == HORIZONTAL;
705 }
706
707 private void resetView() {
Jim Miller521d4002009-11-15 16:19:24 -0800708 mLeftSlider.reset(false);
709 mRightSlider.reset(false);
710 // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
Jim Miller24ccf3c2009-11-10 21:37:45 -0800711 }
712
713 @Override
714 protected void onLayout(boolean changed, int l, int t, int r, int b) {
715 if (!changed) return;
716
717 // Center the widgets in the view
718 mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
719 mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800720 }
721
722 private void moveHandle(float x, float y) {
723 final View handle = mCurrentSlider.tab;
724 final View content = mCurrentSlider.text;
725 if (isHorizontal()) {
726 int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
727 handle.offsetLeftAndRight(deltaX);
728 content.offsetLeftAndRight(deltaX);
729 } else {
730 int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
731 handle.offsetTopAndBottom(deltaY);
732 content.offsetTopAndBottom(deltaY);
733 }
734 invalidate(); // TODO: be more conservative about what we're invalidating
735 }
736
737 /**
738 * Sets the left handle icon to a given resource.
739 *
740 * The resource should refer to a Drawable object, or use 0 to remove
741 * the icon.
742 *
743 * @param iconId the resource ID of the icon drawable
744 * @param targetId the resource of the target drawable
745 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800746 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800747 */
748 public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
749 mLeftSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800750 mLeftSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800751 mLeftSlider.setBarBackgroundResource(barId);
752 mLeftSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800753 mLeftSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800754 }
755
756 /**
757 * Sets the left handle hint text to a given resource string.
758 *
759 * @param resId
760 */
761 public void setLeftHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800762 if (isHorizontal()) {
763 mLeftSlider.setHintText(resId);
764 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800765 }
766
767 /**
768 * Sets the right handle icon to a given resource.
769 *
770 * The resource should refer to a Drawable object, or use 0 to remove
771 * the icon.
772 *
773 * @param iconId the resource ID of the icon drawable
774 * @param targetId the resource of the target drawable
775 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800776 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800777 */
778 public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
779 mRightSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800780 mRightSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800781 mRightSlider.setBarBackgroundResource(barId);
782 mRightSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800783 mRightSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800784 }
785
786 /**
787 * Sets the left handle hint text to a given resource string.
788 *
789 * @param resId
790 */
791 public void setRightHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800792 if (isHorizontal()) {
793 mRightSlider.setHintText(resId);
794 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800795 }
796
Jim Miller4f01d4a2009-11-16 23:08:35 -0800797 public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
798 mHoldLeftOnTransition = holdLeft;
799 mHoldRightOnTransition = holdRight;
800 }
801
Jim Miller24ccf3c2009-11-10 21:37:45 -0800802 /**
803 * Triggers haptic feedback.
804 */
805 private synchronized void vibrate(long duration) {
806 if (mVibrator == null) {
807 mVibrator = (android.os.Vibrator)
808 getContext().getSystemService(Context.VIBRATOR_SERVICE);
809 }
810 mVibrator.vibrate(duration);
811 }
812
813 /**
814 * Registers a callback to be invoked when the user triggers an event.
815 *
816 * @param listener the OnDialTriggerListener to attach to this view
817 */
818 public void setOnTriggerListener(OnTriggerListener listener) {
819 mOnTriggerListener = listener;
820 }
821
822 /**
823 * Dispatches a trigger event to listener. Ignored if a listener is not set.
824 * @param whichHandle the handle that triggered the event.
825 */
826 private void dispatchTriggerEvent(int whichHandle) {
827 vibrate(VIBRATE_LONG);
828 if (mOnTriggerListener != null) {
829 mOnTriggerListener.onTrigger(this, whichHandle);
830 }
831 }
832
833 /**
834 * Sets the current grabbed state, and dispatches a grabbed state change
835 * event to our listener.
836 */
837 private void setGrabbedState(int newState) {
838 if (newState != mGrabbedState) {
839 mGrabbedState = newState;
840 if (mOnTriggerListener != null) {
841 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
842 }
843 }
844 }
845
Jim Miller24ccf3c2009-11-10 21:37:45 -0800846 private void log(String msg) {
847 Log.d(LOG_TAG, msg);
848 }
849}