blob: adafbb41399b1bcd938b0e7c42199751f65ef2e6 [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.Vibrator;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.view.Gravity;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.ViewGroup;
Jim Miller521d4002009-11-15 16:19:24 -080033import android.view.animation.AlphaAnimation;
34import android.view.animation.Animation;
Jim Miller521d4002009-11-15 16:19:24 -080035import android.view.animation.LinearInterpolator;
36import android.view.animation.TranslateAnimation;
37import android.view.animation.Animation.AnimationListener;
Jim Miller24ccf3c2009-11-10 21:37:45 -080038import android.widget.ImageView;
39import android.widget.TextView;
40import android.widget.ImageView.ScaleType;
41import com.android.internal.R;
42
43/**
44 * A special widget containing two Sliders and a threshold for each. Moving either slider beyond
Jim Miller425ca592009-11-13 19:20:28 -080045 * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
Jim Miller4df2c542009-11-12 03:39:14 -080046 * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
Jim Miller425ca592009-11-13 19:20:28 -080047 * Equivalently, selecting a tab will result in a call to
Jim Miller4df2c542009-11-12 03:39:14 -080048 * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
49 * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
Jim Miller24ccf3c2009-11-10 21:37:45 -080050 *
51 */
52public class SlidingTab extends ViewGroup {
53 private static final String LOG_TAG = "SlidingTab";
54 private static final boolean DBG = false;
55 private static final int HORIZONTAL = 0; // as defined in attrs.xml
56 private static final int VERTICAL = 1;
Jim Miller24ccf3c2009-11-10 21:37:45 -080057
58 // TODO: Make these configurable
Jim Miller4df2c542009-11-12 03:39:14 -080059 private static final float THRESHOLD = 2.0f / 3.0f;
Jim Miller24ccf3c2009-11-10 21:37:45 -080060 private static final long VIBRATE_SHORT = 30;
61 private static final long VIBRATE_LONG = 40;
Jim Miller521d4002009-11-15 16:19:24 -080062 private static final int TRACKING_MARGIN = 50;
63 private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
64 private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
Jim Miller4f01d4a2009-11-16 23:08:35 -080065 private boolean mHoldLeftOnTransition = true;
66 private boolean mHoldRightOnTransition = true;
Jim Miller24ccf3c2009-11-10 21:37:45 -080067
68 private OnTriggerListener mOnTriggerListener;
69 private int mGrabbedState = OnTriggerListener.NO_HANDLE;
70 private boolean mTriggered = false;
71 private Vibrator mVibrator;
72 private float mDensity; // used to scale dimensions for bitmaps.
73
Jim Miller24ccf3c2009-11-10 21:37:45 -080074 /**
75 * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
76 */
77 private int mOrientation;
78
79 private Slider mLeftSlider;
80 private Slider mRightSlider;
81 private Slider mCurrentSlider;
82 private boolean mTracking;
Jim Miller4df2c542009-11-12 03:39:14 -080083 private float mThreshold;
Jim Miller24ccf3c2009-11-10 21:37:45 -080084 private Slider mOtherSlider;
85 private boolean mAnimating;
Jim Miller521d4002009-11-15 16:19:24 -080086 private Rect mTmpRect;
Jim Miller24ccf3c2009-11-10 21:37:45 -080087
88 /**
Jim Miller4f01d4a2009-11-16 23:08:35 -080089 * Listener used to reset the view when the current animation completes.
90 */
91 private final AnimationListener mAnimationDoneListener = new AnimationListener() {
92 public void onAnimationStart(Animation animation) {
93
94 }
95
96 public void onAnimationRepeat(Animation animation) {
97
98 }
99
100 public void onAnimationEnd(Animation animation) {
101 onAnimationDone();
102 }
103 };
104
105 /**
Jim Miller24ccf3c2009-11-10 21:37:45 -0800106 * Interface definition for a callback to be invoked when a tab is triggered
Jim Miller4df2c542009-11-12 03:39:14 -0800107 * by moving it beyond a threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800108 */
109 public interface OnTriggerListener {
110 /**
111 * The interface was triggered because the user let go of the handle without reaching the
Jim Miller4df2c542009-11-12 03:39:14 -0800112 * threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800113 */
114 public static final int NO_HANDLE = 0;
115
116 /**
117 * The interface was triggered because the user grabbed the left handle and moved it past
Jim Miller4df2c542009-11-12 03:39:14 -0800118 * the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800119 */
120 public static final int LEFT_HANDLE = 1;
121
122 /**
123 * The interface was triggered because the user grabbed the right handle and moved it past
Jim Miller4df2c542009-11-12 03:39:14 -0800124 * the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800125 */
126 public static final int RIGHT_HANDLE = 2;
127
128 /**
Jim Miller4df2c542009-11-12 03:39:14 -0800129 * Called when the user moves a handle beyond the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800130 *
131 * @param v The view that was triggered.
132 * @param whichHandle Which "dial handle" the user grabbed,
133 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
134 */
135 void onTrigger(View v, int whichHandle);
136
137 /**
138 * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
139 * one of the handles.)
140 *
141 * @param v the view that was triggered
142 * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
143 * or {@link #RIGHT_HANDLE}.
144 */
145 void onGrabbedStateChange(View v, int grabbedState);
146 }
147
Jim Miller521d4002009-11-15 16:19:24 -0800148 // TODO: For debugging; remove after glitches debugged.
149 @Override
150 protected void dispatchDraw(Canvas canvas) {
Jim Miller1d0a1522009-11-18 01:35:42 -0800151 int orientation = getResources().getConfiguration().orientation;
152 if (mOrientation == HORIZONTAL && orientation != Configuration.ORIENTATION_PORTRAIT
153 || mOrientation == VERTICAL && orientation != Configuration.ORIENTATION_LANDSCAPE) {
154 // UBER HACK ALERT. This is a workaround for a configuration race condition between
155 // orientation changed notification and the resize notification. This just prevents
156 // us from drawing under this circumstance, though the view will still be wrong.
157 return;
158 }
Jim Miller521d4002009-11-15 16:19:24 -0800159 super.dispatchDraw(canvas);
160 }
161
Jim Miller24ccf3c2009-11-10 21:37:45 -0800162 /**
Jim Miller425ca592009-11-13 19:20:28 -0800163 * Simple container class for all things pertinent to a slider.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800164 * A slider consists of 3 Views:
Jim Miller425ca592009-11-13 19:20:28 -0800165 *
Jim Miller24ccf3c2009-11-10 21:37:45 -0800166 * {@link #tab} is the tab shown on the screen in the default state.
167 * {@link #text} is the view revealed as the user slides the tab out.
168 * {@link #target} is the target the user must drag the slider past to trigger the slider.
169 *
170 */
171 private static class Slider {
172 /**
173 * Tab alignment - determines which side the tab should be drawn on
174 */
175 public static final int ALIGN_LEFT = 0;
176 public static final int ALIGN_RIGHT = 1;
177 public static final int ALIGN_TOP = 2;
178 public static final int ALIGN_BOTTOM = 3;
Jim Miller521d4002009-11-15 16:19:24 -0800179 public static final int ALIGN_UNKNOWN = 4;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800180
181 /**
182 * States for the view.
183 */
184 private static final int STATE_NORMAL = 0;
185 private static final int STATE_PRESSED = 1;
186 private static final int STATE_ACTIVE = 2;
187
188 private final ImageView tab;
189 private final TextView text;
190 private final ImageView target;
Jim Miller4df2c542009-11-12 03:39:14 -0800191 private int currentState = STATE_NORMAL;
Jim Miller521d4002009-11-15 16:19:24 -0800192 private int alignment = ALIGN_UNKNOWN;
193 private int alignment_value;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800194
195 /**
196 * Constructor
Jim Miller425ca592009-11-13 19:20:28 -0800197 *
Jim Miller24ccf3c2009-11-10 21:37:45 -0800198 * @param parent the container view of this one
199 * @param tabId drawable for the tab
200 * @param barId drawable for the bar
201 * @param targetId drawable for the target
202 */
203 Slider(ViewGroup parent, int tabId, int barId, int targetId) {
204 // Create tab
205 tab = new ImageView(parent.getContext());
206 tab.setBackgroundResource(tabId);
207 tab.setScaleType(ScaleType.CENTER);
208 tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
209 LayoutParams.WRAP_CONTENT));
210
211 // Create hint TextView
212 text = new TextView(parent.getContext());
213 text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
Romain Guy980a9382010-01-08 15:06:28 -0800214 LayoutParams.MATCH_PARENT));
Jim Miller24ccf3c2009-11-10 21:37:45 -0800215 text.setBackgroundResource(barId);
216 text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
217 // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen
218
219 // Create target
220 target = new ImageView(parent.getContext());
221 target.setImageResource(targetId);
222 target.setScaleType(ScaleType.CENTER);
223 target.setLayoutParams(
224 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
225 target.setVisibility(View.INVISIBLE);
226
227 parent.addView(target); // this needs to be first - relies on painter's algorithm
228 parent.addView(tab);
229 parent.addView(text);
230 }
231
232 void setIcon(int iconId) {
233 tab.setImageResource(iconId);
234 }
Jim Miller425ca592009-11-13 19:20:28 -0800235
Jim Miller24ccf3c2009-11-10 21:37:45 -0800236 void setTabBackgroundResource(int tabId) {
237 tab.setBackgroundResource(tabId);
238 }
Jim Miller425ca592009-11-13 19:20:28 -0800239
Jim Miller24ccf3c2009-11-10 21:37:45 -0800240 void setBarBackgroundResource(int barId) {
241 text.setBackgroundResource(barId);
242 }
Jim Miller425ca592009-11-13 19:20:28 -0800243
Jim Miller24ccf3c2009-11-10 21:37:45 -0800244 void setHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800245 text.setText(resId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800246 }
247
248 void hide() {
Jim Miller521d4002009-11-15 16:19:24 -0800249 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
250 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
251 : alignment_value - tab.getLeft()) : 0;
252 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
253 : alignment_value - tab.getTop());
254
255 Animation trans = new TranslateAnimation(0, dx, 0, dy);
256 trans.setDuration(ANIM_DURATION);
257 trans.setFillAfter(true);
258 tab.startAnimation(trans);
259 text.startAnimation(trans);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800260 target.setVisibility(View.INVISIBLE);
261 }
262
Jim Miller521d4002009-11-15 16:19:24 -0800263 void show(boolean animate) {
264 text.setVisibility(View.VISIBLE);
265 tab.setVisibility(View.VISIBLE);
266 //target.setVisibility(View.INVISIBLE);
267 if (animate) {
268 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
269 int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
270 int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
271
272 Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
273 trans.setDuration(ANIM_DURATION);
274 tab.startAnimation(trans);
275 text.startAnimation(trans);
276 }
277 }
278
Jim Miller24ccf3c2009-11-10 21:37:45 -0800279 void setState(int state) {
280 text.setPressed(state == STATE_PRESSED);
281 tab.setPressed(state == STATE_PRESSED);
282 if (state == STATE_ACTIVE) {
283 final int[] activeState = new int[] {com.android.internal.R.attr.state_active};
284 if (text.getBackground().isStateful()) {
285 text.getBackground().setState(activeState);
286 }
287 if (tab.getBackground().isStateful()) {
288 tab.getBackground().setState(activeState);
289 }
290 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
291 } else {
292 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
293 }
Jim Miller4df2c542009-11-12 03:39:14 -0800294 currentState = state;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800295 }
296
297 void showTarget() {
Jim Miller521d4002009-11-15 16:19:24 -0800298 AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
299 alphaAnim.setDuration(ANIM_TARGET_TIME);
300 target.startAnimation(alphaAnim);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800301 target.setVisibility(View.VISIBLE);
302 }
303
Jim Miller521d4002009-11-15 16:19:24 -0800304 void reset(boolean animate) {
Jim Miller24ccf3c2009-11-10 21:37:45 -0800305 setState(STATE_NORMAL);
306 text.setVisibility(View.VISIBLE);
307 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
308 tab.setVisibility(View.VISIBLE);
309 target.setVisibility(View.INVISIBLE);
Jim Miller521d4002009-11-15 16:19:24 -0800310 final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
311 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getLeft()
312 : alignment_value - tab.getRight()) : 0;
313 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
314 : alignment_value - tab.getBottom());
315 if (animate) {
316 TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
317 trans.setDuration(ANIM_DURATION);
318 trans.setFillAfter(false);
319 text.startAnimation(trans);
320 tab.startAnimation(trans);
321 } else {
322 if (horiz) {
323 text.offsetLeftAndRight(dx);
324 tab.offsetLeftAndRight(dx);
325 } else {
326 text.offsetTopAndBottom(dy);
327 tab.offsetTopAndBottom(dy);
328 }
Jim Miller4f01d4a2009-11-16 23:08:35 -0800329 text.clearAnimation();
330 tab.clearAnimation();
331 target.clearAnimation();
Jim Miller521d4002009-11-15 16:19:24 -0800332 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800333 }
334
335 void setTarget(int targetId) {
336 target.setImageResource(targetId);
337 }
338
339 /**
340 * Layout the given widgets within the parent.
341 *
342 * @param l the parent's left border
343 * @param t the parent's top border
344 * @param r the parent's right border
345 * @param b the parent's bottom border
346 * @param alignment which side to align the widget to
347 */
348 void layout(int l, int t, int r, int b, int alignment) {
Jim Miller521d4002009-11-15 16:19:24 -0800349 this.alignment = alignment;
Jim Miller753401a2009-11-11 17:53:53 -0800350 final Drawable tabBackground = tab.getBackground();
351 final int handleWidth = tabBackground.getIntrinsicWidth();
352 final int handleHeight = tabBackground.getIntrinsicHeight();
353 final Drawable targetDrawable = target.getDrawable();
354 final int targetWidth = targetDrawable.getIntrinsicWidth();
355 final int targetHeight = targetDrawable.getIntrinsicHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800356 final int parentWidth = r - l;
357 final int parentHeight = b - t;
358
Jim Miller4df2c542009-11-12 03:39:14 -0800359 final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
360 final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800361 final int left = (parentWidth - handleWidth) / 2;
362 final int right = left + handleWidth;
363
364 if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
365 // horizontal
366 final int targetTop = (parentHeight - targetHeight) / 2;
367 final int targetBottom = targetTop + targetHeight;
368 final int top = (parentHeight - handleHeight) / 2;
369 final int bottom = (parentHeight + handleHeight) / 2;
370 if (alignment == ALIGN_LEFT) {
371 tab.layout(0, top, handleWidth, bottom);
372 text.layout(0 - parentWidth, top, 0, bottom);
373 text.setGravity(Gravity.RIGHT);
374 target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
Jim Miller521d4002009-11-15 16:19:24 -0800375 alignment_value = l;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800376 } else {
377 tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
378 text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
379 target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
380 text.setGravity(Gravity.TOP);
Jim Miller521d4002009-11-15 16:19:24 -0800381 alignment_value = r;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800382 }
383 } else {
384 // vertical
385 final int targetLeft = (parentWidth - targetWidth) / 2;
386 final int targetRight = (parentWidth + targetWidth) / 2;
Jim Miller4df2c542009-11-12 03:39:14 -0800387 final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
388 final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800389 if (alignment == ALIGN_TOP) {
390 tab.layout(left, 0, right, handleHeight);
391 text.layout(left, 0 - parentHeight, right, 0);
392 target.layout(targetLeft, top, targetRight, top + targetHeight);
Jim Miller521d4002009-11-15 16:19:24 -0800393 alignment_value = t;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800394 } else {
395 tab.layout(left, parentHeight - handleHeight, right, parentHeight);
396 text.layout(left, parentHeight, right, parentHeight + parentHeight);
397 target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
Jim Miller521d4002009-11-15 16:19:24 -0800398 alignment_value = b;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800399 }
400 }
401 }
402
Jim Miller4df2c542009-11-12 03:39:14 -0800403 public void updateDrawableStates() {
404 setState(currentState);
405 }
406
Jim Miller4811d622009-11-12 17:45:07 -0800407 /**
408 * Ensure all the dependent widgets are measured.
409 */
410 public void measure() {
411 tab.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
412 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
413 text.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
414 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
Jim Miller24ccf3c2009-11-10 21:37:45 -0800415 }
416
Jim Miller4811d622009-11-12 17:45:07 -0800417 /**
418 * Get the measured tab width. Must be called after {@link Slider#measure()}.
419 * @return
420 */
421 public int getTabWidth() {
422 return tab.getMeasuredWidth();
423 }
424
425 /**
426 * Get the measured tab width. Must be called after {@link Slider#measure()}.
427 * @return
428 */
Jim Miller24ccf3c2009-11-10 21:37:45 -0800429 public int getTabHeight() {
Jim Miller4811d622009-11-12 17:45:07 -0800430 return tab.getMeasuredHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800431 }
Jim Miller521d4002009-11-15 16:19:24 -0800432
Jim Miller8b63ab62009-12-02 13:42:48 -0800433 /**
434 * Start animating the slider. Note we need two animations since an Animator
435 * keeps internal state of the invalidation region which is just the view being animated.
436 *
437 * @param anim1
438 * @param anim2
439 */
440 public void startAnimation(Animation anim1, Animation anim2) {
441 tab.startAnimation(anim1);
442 text.startAnimation(anim2);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800443 }
444
445 public void hideTarget() {
446 target.clearAnimation();
447 target.setVisibility(View.INVISIBLE);
Jim Miller521d4002009-11-15 16:19:24 -0800448 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800449 }
450
451 public SlidingTab(Context context) {
452 this(context, null);
453 }
454
455 /**
456 * Constructor used when this widget is created from a layout file.
457 */
458 public SlidingTab(Context context, AttributeSet attrs) {
459 super(context, attrs);
460
Jim Miller521d4002009-11-15 16:19:24 -0800461 // Allocate a temporary once that can be used everywhere.
462 mTmpRect = new Rect();
463
Jim Miller24ccf3c2009-11-10 21:37:45 -0800464 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
465 mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
466 a.recycle();
467
468 Resources r = getResources();
469 mDensity = r.getDisplayMetrics().density;
470 if (DBG) log("- Density: " + mDensity);
471
Jim Miller425ca592009-11-13 19:20:28 -0800472 mLeftSlider = new Slider(this,
473 R.drawable.jog_tab_left_generic,
Jim Miller24ccf3c2009-11-10 21:37:45 -0800474 R.drawable.jog_tab_bar_left_generic,
475 R.drawable.jog_tab_target_gray);
Jim Miller425ca592009-11-13 19:20:28 -0800476 mRightSlider = new Slider(this,
477 R.drawable.jog_tab_right_generic,
Jim Miller24ccf3c2009-11-10 21:37:45 -0800478 R.drawable.jog_tab_bar_right_generic,
479 R.drawable.jog_tab_target_gray);
480
481 // setBackgroundColor(0x80808080);
482 }
483
484 @Override
485 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
486 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
487 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
488
489 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
490 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
491
492 if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
493 throw new RuntimeException(LOG_TAG + " cannot have UNSPECIFIED dimensions");
494 }
495
Jim Miller4811d622009-11-12 17:45:07 -0800496 mLeftSlider.measure();
497 mRightSlider.measure();
498 final int leftTabWidth = mLeftSlider.getTabWidth();
499 final int rightTabWidth = mRightSlider.getTabWidth();
500 final int leftTabHeight = mLeftSlider.getTabHeight();
501 final int rightTabHeight = mRightSlider.getTabHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800502 final int width;
503 final int height;
504 if (isHorizontal()) {
505 width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
506 height = Math.max(leftTabHeight, rightTabHeight);
507 } else {
508 width = Math.max(leftTabWidth, rightTabHeight);
509 height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
510 }
511 setMeasuredDimension(width, height);
512 }
513
514 @Override
515 public boolean onInterceptTouchEvent(MotionEvent event) {
516 final int action = event.getAction();
517 final float x = event.getX();
518 final float y = event.getY();
519
Jim Miller24ccf3c2009-11-10 21:37:45 -0800520 if (mAnimating) {
521 return false;
522 }
523
524 View leftHandle = mLeftSlider.tab;
Jim Miller521d4002009-11-15 16:19:24 -0800525 leftHandle.getHitRect(mTmpRect);
526 boolean leftHit = mTmpRect.contains((int) x, (int) y);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800527
528 View rightHandle = mRightSlider.tab;
Jim Miller521d4002009-11-15 16:19:24 -0800529 rightHandle.getHitRect(mTmpRect);
530 boolean rightHit = mTmpRect.contains((int)x, (int) y);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800531
532 if (!mTracking && !(leftHit || rightHit)) {
533 return false;
534 }
535
536 switch (action) {
537 case MotionEvent.ACTION_DOWN: {
538 mTracking = true;
539 mTriggered = false;
540 vibrate(VIBRATE_SHORT);
541 if (leftHit) {
542 mCurrentSlider = mLeftSlider;
543 mOtherSlider = mRightSlider;
Jim Miller4df2c542009-11-12 03:39:14 -0800544 mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800545 setGrabbedState(OnTriggerListener.LEFT_HANDLE);
546 } else {
547 mCurrentSlider = mRightSlider;
548 mOtherSlider = mLeftSlider;
Jim Miller4df2c542009-11-12 03:39:14 -0800549 mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800550 setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
551 }
552 mCurrentSlider.setState(Slider.STATE_PRESSED);
553 mCurrentSlider.showTarget();
554 mOtherSlider.hide();
555 break;
556 }
557 }
558
559 return true;
560 }
561
562 @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) {
566 mLeftSlider.reset(false);
567 mRightSlider.reset(false);
568 }
569 super.setVisibility(visibility);
570 }
571
572 @Override
Jim Miller24ccf3c2009-11-10 21:37:45 -0800573 public boolean onTouchEvent(MotionEvent event) {
574 if (mTracking) {
575 final int action = event.getAction();
576 final float x = event.getX();
577 final float y = event.getY();
Jim Miller521d4002009-11-15 16:19:24 -0800578
Jim Miller24ccf3c2009-11-10 21:37:45 -0800579 switch (action) {
580 case MotionEvent.ACTION_MOVE:
Jim Miller521d4002009-11-15 16:19:24 -0800581 if (withinView(x, y, this) ) {
582 moveHandle(x, y);
583 float position = isHorizontal() ? x : y;
584 float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
585 boolean thresholdReached;
586 if (isHorizontal()) {
587 thresholdReached = mCurrentSlider == mLeftSlider ?
588 position > target : position < target;
589 } else {
590 thresholdReached = mCurrentSlider == mLeftSlider ?
591 position < target : position > target;
592 }
593 if (!mTriggered && thresholdReached) {
594 mTriggered = true;
595 mTracking = false;
596 mCurrentSlider.setState(Slider.STATE_ACTIVE);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800597 boolean isLeft = mCurrentSlider == mLeftSlider;
598 dispatchTriggerEvent(isLeft ?
Jim Miller521d4002009-11-15 16:19:24 -0800599 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800600
Jim Miller4f01d4a2009-11-16 23:08:35 -0800601 startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
Jim Miller521d4002009-11-15 16:19:24 -0800602 setGrabbedState(OnTriggerListener.NO_HANDLE);
603 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800604 break;
605 }
606 // Intentionally fall through - we're outside tracking rectangle
607
608 case MotionEvent.ACTION_UP:
609 case MotionEvent.ACTION_CANCEL:
610 mTracking = false;
611 mTriggered = false;
Jim Miller521d4002009-11-15 16:19:24 -0800612 mOtherSlider.show(true);
613 mCurrentSlider.reset(false);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800614 mCurrentSlider.hideTarget();
Jim Miller521d4002009-11-15 16:19:24 -0800615 mCurrentSlider = null;
616 mOtherSlider = null;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800617 setGrabbedState(OnTriggerListener.NO_HANDLE);
618 break;
619 }
620 }
621
622 return mTracking || super.onTouchEvent(event);
623 }
624
Jim Miller4f01d4a2009-11-16 23:08:35 -0800625 void startAnimating(final boolean holdAfter) {
Jim Miller521d4002009-11-15 16:19:24 -0800626 mAnimating = true;
Jim Miller8b63ab62009-12-02 13:42:48 -0800627 final Animation trans1;
628 final Animation trans2;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800629 final Slider slider = mCurrentSlider;
630 final Slider other = mOtherSlider;
631 final int dx;
632 final int dy;
Jim Miller521d4002009-11-15 16:19:24 -0800633 if (isHorizontal()) {
634 int right = slider.tab.getRight();
635 int width = slider.tab.getWidth();
636 int left = slider.tab.getLeft();
637 int viewWidth = getWidth();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800638 int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
639 dx = slider == mRightSlider ? - (right + viewWidth - holdOffset)
640 : (viewWidth - left) + viewWidth - holdOffset;
Jim Miller521d4002009-11-15 16:19:24 -0800641 dy = 0;
642 } else {
643 int top = slider.tab.getTop();
644 int bottom = slider.tab.getBottom();
645 int height = slider.tab.getHeight();
646 int viewHeight = getHeight();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800647 int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
Jim Miller521d4002009-11-15 16:19:24 -0800648 dx = 0;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800649 dy = slider == mRightSlider ? (top + viewHeight - holdOffset)
650 : - ((viewHeight - bottom) + viewHeight - holdOffset);
Jim Miller521d4002009-11-15 16:19:24 -0800651 }
Jim Miller8b63ab62009-12-02 13:42:48 -0800652 trans1 = new TranslateAnimation(0, dx, 0, dy);
653 trans1.setDuration(ANIM_DURATION);
654 trans1.setInterpolator(new LinearInterpolator());
655 trans1.setFillAfter(true);
656 trans2 = new TranslateAnimation(0, dx, 0, dy);
657 trans2.setDuration(ANIM_DURATION);
658 trans2.setInterpolator(new LinearInterpolator());
659 trans2.setFillAfter(true);
Jim Miller521d4002009-11-15 16:19:24 -0800660
Jim Miller8b63ab62009-12-02 13:42:48 -0800661 trans1.setAnimationListener(new AnimationListener() {
Jim Miller521d4002009-11-15 16:19:24 -0800662 public void onAnimationEnd(Animation animation) {
Jim Miller4f01d4a2009-11-16 23:08:35 -0800663 Animation anim;
664 if (holdAfter) {
665 anim = new TranslateAnimation(dx, dx, dy, dy);
666 anim.setDuration(1000); // plenty of time for transitions
667 mAnimating = false;
668 } else {
669 anim = new AlphaAnimation(0.5f, 1.0f);
670 anim.setDuration(ANIM_DURATION);
671 resetView();
672 }
673 anim.setAnimationListener(mAnimationDoneListener);
Jim Miller8b63ab62009-12-02 13:42:48 -0800674
675 /* Animation can be the same for these since the animation just holds */
676 mLeftSlider.startAnimation(anim, anim);
677 mRightSlider.startAnimation(anim, anim);
Jim Miller521d4002009-11-15 16:19:24 -0800678 }
679
680 public void onAnimationRepeat(Animation animation) {
681
682 }
683
684 public void onAnimationStart(Animation animation) {
685
686 }
687
688 });
689
Jim Miller4f01d4a2009-11-16 23:08:35 -0800690 slider.hideTarget();
Jim Miller8b63ab62009-12-02 13:42:48 -0800691 slider.startAnimation(trans1, trans2);
Jim Miller521d4002009-11-15 16:19:24 -0800692 }
693
Jim Miller4f01d4a2009-11-16 23:08:35 -0800694 private void onAnimationDone() {
695 resetView();
696 mAnimating = false;
697 }
698
Jim Miller521d4002009-11-15 16:19:24 -0800699 private boolean withinView(final float x, final float y, final View view) {
700 return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
701 || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
702 }
703
Jim Miller24ccf3c2009-11-10 21:37:45 -0800704 private boolean isHorizontal() {
705 return mOrientation == HORIZONTAL;
706 }
707
708 private void resetView() {
Jim Miller521d4002009-11-15 16:19:24 -0800709 mLeftSlider.reset(false);
710 mRightSlider.reset(false);
711 // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
Jim Miller24ccf3c2009-11-10 21:37:45 -0800712 }
713
714 @Override
715 protected void onLayout(boolean changed, int l, int t, int r, int b) {
716 if (!changed) return;
717
718 // Center the widgets in the view
719 mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
720 mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800721 }
722
723 private void moveHandle(float x, float y) {
724 final View handle = mCurrentSlider.tab;
725 final View content = mCurrentSlider.text;
726 if (isHorizontal()) {
727 int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
728 handle.offsetLeftAndRight(deltaX);
729 content.offsetLeftAndRight(deltaX);
730 } else {
731 int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
732 handle.offsetTopAndBottom(deltaY);
733 content.offsetTopAndBottom(deltaY);
734 }
735 invalidate(); // TODO: be more conservative about what we're invalidating
736 }
737
738 /**
739 * Sets the left handle icon to a given resource.
740 *
741 * The resource should refer to a Drawable object, or use 0 to remove
742 * the icon.
743 *
744 * @param iconId the resource ID of the icon drawable
745 * @param targetId the resource of the target drawable
746 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800747 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800748 */
749 public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
750 mLeftSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800751 mLeftSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800752 mLeftSlider.setBarBackgroundResource(barId);
753 mLeftSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800754 mLeftSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800755 }
756
757 /**
758 * Sets the left handle hint text to a given resource string.
759 *
760 * @param resId
761 */
762 public void setLeftHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800763 if (isHorizontal()) {
764 mLeftSlider.setHintText(resId);
765 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800766 }
767
768 /**
769 * Sets the right handle icon to a given resource.
770 *
771 * The resource should refer to a Drawable object, or use 0 to remove
772 * the icon.
773 *
774 * @param iconId the resource ID of the icon drawable
775 * @param targetId the resource of the target drawable
776 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800777 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800778 */
779 public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
780 mRightSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800781 mRightSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800782 mRightSlider.setBarBackgroundResource(barId);
783 mRightSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800784 mRightSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800785 }
786
787 /**
788 * Sets the left handle hint text to a given resource string.
789 *
790 * @param resId
791 */
792 public void setRightHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800793 if (isHorizontal()) {
794 mRightSlider.setHintText(resId);
795 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800796 }
797
Jim Miller4f01d4a2009-11-16 23:08:35 -0800798 public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
799 mHoldLeftOnTransition = holdLeft;
800 mHoldRightOnTransition = holdRight;
801 }
802
Jim Miller24ccf3c2009-11-10 21:37:45 -0800803 /**
804 * Triggers haptic feedback.
805 */
806 private synchronized void vibrate(long duration) {
807 if (mVibrator == null) {
808 mVibrator = (android.os.Vibrator)
809 getContext().getSystemService(Context.VIBRATOR_SERVICE);
810 }
811 mVibrator.vibrate(duration);
812 }
813
814 /**
815 * Registers a callback to be invoked when the user triggers an event.
816 *
817 * @param listener the OnDialTriggerListener to attach to this view
818 */
819 public void setOnTriggerListener(OnTriggerListener listener) {
820 mOnTriggerListener = listener;
821 }
822
823 /**
824 * Dispatches a trigger event to listener. Ignored if a listener is not set.
825 * @param whichHandle the handle that triggered the event.
826 */
827 private void dispatchTriggerEvent(int whichHandle) {
828 vibrate(VIBRATE_LONG);
829 if (mOnTriggerListener != null) {
830 mOnTriggerListener.onTrigger(this, whichHandle);
831 }
832 }
833
834 /**
835 * Sets the current grabbed state, and dispatches a grabbed state change
836 * event to our listener.
837 */
838 private void setGrabbedState(int newState) {
839 if (newState != mGrabbedState) {
840 mGrabbedState = newState;
841 if (mOnTriggerListener != null) {
842 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
843 }
844 }
845 }
846
Jim Miller24ccf3c2009-11-10 21:37:45 -0800847 private void log(String msg) {
848 Log.d(LOG_TAG, msg);
849 }
850}