blob: 65feab174e9a39164f1f548682d890b98ed4cbe5 [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;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.graphics.Rect;
Jim Miller753401a2009-11-11 17:53:53 -080023import android.graphics.drawable.Drawable;
John Spurlock7b414672014-07-18 13:02:39 -040024import android.media.AudioAttributes;
Jeff Sharkey723a7252012-10-12 14:26:31 -070025import android.os.UserHandle;
Jim Miller24ccf3c2009-11-10 21:37:45 -080026import android.os.Vibrator;
Jeff Sharkey723a7252012-10-12 14:26:31 -070027import android.provider.Settings;
Jim Miller24ccf3c2009-11-10 21:37:45 -080028import android.util.AttributeSet;
29import android.util.Log;
30import android.view.Gravity;
31import android.view.MotionEvent;
32import android.view.View;
33import android.view.ViewGroup;
Jim Miller521d4002009-11-15 16:19:24 -080034import android.view.animation.AlphaAnimation;
35import android.view.animation.Animation;
Jim Miller521d4002009-11-15 16:19:24 -080036import android.view.animation.LinearInterpolator;
37import android.view.animation.TranslateAnimation;
38import android.view.animation.Animation.AnimationListener;
Jim Miller24ccf3c2009-11-10 21:37:45 -080039import android.widget.ImageView;
40import android.widget.TextView;
41import android.widget.ImageView.ScaleType;
Jim Miller92c15712010-05-25 19:09:44 -070042
Jim Miller24ccf3c2009-11-10 21:37:45 -080043import com.android.internal.R;
44
45/**
46 * A special widget containing two Sliders and a threshold for each. Moving either slider beyond
Jim Miller425ca592009-11-13 19:20:28 -080047 * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
Jim Miller4df2c542009-11-12 03:39:14 -080048 * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
Jim Miller425ca592009-11-13 19:20:28 -080049 * Equivalently, selecting a tab will result in a call to
Jim Miller4df2c542009-11-12 03:39:14 -080050 * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
51 * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
Jim Miller24ccf3c2009-11-10 21:37:45 -080052 *
53 */
54public class SlidingTab extends ViewGroup {
55 private static final String LOG_TAG = "SlidingTab";
56 private static final boolean DBG = false;
57 private static final int HORIZONTAL = 0; // as defined in attrs.xml
58 private static final int VERTICAL = 1;
Jim Miller24ccf3c2009-11-10 21:37:45 -080059
60 // TODO: Make these configurable
Jim Miller4df2c542009-11-12 03:39:14 -080061 private static final float THRESHOLD = 2.0f / 3.0f;
Jim Miller24ccf3c2009-11-10 21:37:45 -080062 private static final long VIBRATE_SHORT = 30;
63 private static final long VIBRATE_LONG = 40;
Jim Miller521d4002009-11-15 16:19:24 -080064 private static final int TRACKING_MARGIN = 50;
65 private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
66 private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
Jim Miller4f01d4a2009-11-16 23:08:35 -080067 private boolean mHoldLeftOnTransition = true;
68 private boolean mHoldRightOnTransition = true;
Jim Miller24ccf3c2009-11-10 21:37:45 -080069
John Spurlock7b414672014-07-18 13:02:39 -040070 private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
71 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
72 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
73 .build();
74
Jim Miller24ccf3c2009-11-10 21:37:45 -080075 private OnTriggerListener mOnTriggerListener;
76 private int mGrabbedState = OnTriggerListener.NO_HANDLE;
77 private boolean mTriggered = false;
78 private Vibrator mVibrator;
Jim Miller92c15712010-05-25 19:09:44 -070079 private final float mDensity; // used to scale dimensions for bitmaps.
Jim Miller24ccf3c2009-11-10 21:37:45 -080080
Jim Miller24ccf3c2009-11-10 21:37:45 -080081 /**
82 * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
83 */
Jim Miller92c15712010-05-25 19:09:44 -070084 private final int mOrientation;
Jim Miller24ccf3c2009-11-10 21:37:45 -080085
Jim Miller92c15712010-05-25 19:09:44 -070086 private final Slider mLeftSlider;
87 private final Slider mRightSlider;
Jim Miller24ccf3c2009-11-10 21:37:45 -080088 private Slider mCurrentSlider;
89 private boolean mTracking;
Jim Miller4df2c542009-11-12 03:39:14 -080090 private float mThreshold;
Jim Miller24ccf3c2009-11-10 21:37:45 -080091 private Slider mOtherSlider;
92 private boolean mAnimating;
Jim Miller92c15712010-05-25 19:09:44 -070093 private final Rect mTmpRect;
Jim Miller24ccf3c2009-11-10 21:37:45 -080094
95 /**
Jim Miller4f01d4a2009-11-16 23:08:35 -080096 * Listener used to reset the view when the current animation completes.
97 */
98 private final AnimationListener mAnimationDoneListener = new AnimationListener() {
99 public void onAnimationStart(Animation animation) {
100
101 }
102
103 public void onAnimationRepeat(Animation animation) {
104
105 }
106
107 public void onAnimationEnd(Animation animation) {
108 onAnimationDone();
109 }
110 };
111
112 /**
Jim Miller24ccf3c2009-11-10 21:37:45 -0800113 * Interface definition for a callback to be invoked when a tab is triggered
Jim Miller4df2c542009-11-12 03:39:14 -0800114 * by moving it beyond a threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800115 */
116 public interface OnTriggerListener {
117 /**
118 * The interface was triggered because the user let go of the handle without reaching the
Jim Miller4df2c542009-11-12 03:39:14 -0800119 * threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800120 */
121 public static final int NO_HANDLE = 0;
122
123 /**
124 * The interface was triggered because the user grabbed the left handle and moved it past
Jim Miller4df2c542009-11-12 03:39:14 -0800125 * the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800126 */
127 public static final int LEFT_HANDLE = 1;
128
129 /**
130 * The interface was triggered because the user grabbed the right handle and moved it past
Jim Miller4df2c542009-11-12 03:39:14 -0800131 * the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800132 */
133 public static final int RIGHT_HANDLE = 2;
134
135 /**
Jim Miller4df2c542009-11-12 03:39:14 -0800136 * Called when the user moves a handle beyond the threshold.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800137 *
138 * @param v The view that was triggered.
139 * @param whichHandle Which "dial handle" the user grabbed,
140 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
141 */
142 void onTrigger(View v, int whichHandle);
143
144 /**
145 * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
146 * one of the handles.)
147 *
148 * @param v the view that was triggered
149 * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
150 * or {@link #RIGHT_HANDLE}.
151 */
152 void onGrabbedStateChange(View v, int grabbedState);
153 }
154
155 /**
Jim Miller425ca592009-11-13 19:20:28 -0800156 * Simple container class for all things pertinent to a slider.
Jim Miller24ccf3c2009-11-10 21:37:45 -0800157 * A slider consists of 3 Views:
Jim Miller425ca592009-11-13 19:20:28 -0800158 *
Jim Miller24ccf3c2009-11-10 21:37:45 -0800159 * {@link #tab} is the tab shown on the screen in the default state.
160 * {@link #text} is the view revealed as the user slides the tab out.
161 * {@link #target} is the target the user must drag the slider past to trigger the slider.
162 *
163 */
164 private static class Slider {
165 /**
166 * Tab alignment - determines which side the tab should be drawn on
167 */
168 public static final int ALIGN_LEFT = 0;
169 public static final int ALIGN_RIGHT = 1;
170 public static final int ALIGN_TOP = 2;
171 public static final int ALIGN_BOTTOM = 3;
Jim Miller521d4002009-11-15 16:19:24 -0800172 public static final int ALIGN_UNKNOWN = 4;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800173
174 /**
175 * States for the view.
176 */
177 private static final int STATE_NORMAL = 0;
178 private static final int STATE_PRESSED = 1;
179 private static final int STATE_ACTIVE = 2;
180
181 private final ImageView tab;
182 private final TextView text;
183 private final ImageView target;
Jim Miller4df2c542009-11-12 03:39:14 -0800184 private int currentState = STATE_NORMAL;
Jim Miller521d4002009-11-15 16:19:24 -0800185 private int alignment = ALIGN_UNKNOWN;
186 private int alignment_value;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800187
188 /**
189 * Constructor
Jim Miller425ca592009-11-13 19:20:28 -0800190 *
Jim Miller24ccf3c2009-11-10 21:37:45 -0800191 * @param parent the container view of this one
192 * @param tabId drawable for the tab
193 * @param barId drawable for the bar
194 * @param targetId drawable for the target
195 */
196 Slider(ViewGroup parent, int tabId, int barId, int targetId) {
197 // Create tab
198 tab = new ImageView(parent.getContext());
199 tab.setBackgroundResource(tabId);
200 tab.setScaleType(ScaleType.CENTER);
201 tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
202 LayoutParams.WRAP_CONTENT));
203
204 // Create hint TextView
205 text = new TextView(parent.getContext());
206 text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
Romain Guy980a9382010-01-08 15:06:28 -0800207 LayoutParams.MATCH_PARENT));
Jim Miller24ccf3c2009-11-10 21:37:45 -0800208 text.setBackgroundResource(barId);
209 text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
210 // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen
211
212 // Create target
213 target = new ImageView(parent.getContext());
214 target.setImageResource(targetId);
215 target.setScaleType(ScaleType.CENTER);
216 target.setLayoutParams(
217 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
218 target.setVisibility(View.INVISIBLE);
219
220 parent.addView(target); // this needs to be first - relies on painter's algorithm
221 parent.addView(tab);
222 parent.addView(text);
223 }
224
225 void setIcon(int iconId) {
226 tab.setImageResource(iconId);
227 }
Jim Miller425ca592009-11-13 19:20:28 -0800228
Jim Miller24ccf3c2009-11-10 21:37:45 -0800229 void setTabBackgroundResource(int tabId) {
230 tab.setBackgroundResource(tabId);
231 }
Jim Miller425ca592009-11-13 19:20:28 -0800232
Jim Miller24ccf3c2009-11-10 21:37:45 -0800233 void setBarBackgroundResource(int barId) {
234 text.setBackgroundResource(barId);
235 }
Jim Miller425ca592009-11-13 19:20:28 -0800236
Jim Miller24ccf3c2009-11-10 21:37:45 -0800237 void setHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800238 text.setText(resId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800239 }
240
241 void hide() {
Jim Miller521d4002009-11-15 16:19:24 -0800242 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
243 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
244 : alignment_value - tab.getLeft()) : 0;
245 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
246 : alignment_value - tab.getTop());
247
248 Animation trans = new TranslateAnimation(0, dx, 0, dy);
249 trans.setDuration(ANIM_DURATION);
250 trans.setFillAfter(true);
251 tab.startAnimation(trans);
252 text.startAnimation(trans);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800253 target.setVisibility(View.INVISIBLE);
254 }
255
Jim Miller521d4002009-11-15 16:19:24 -0800256 void show(boolean animate) {
257 text.setVisibility(View.VISIBLE);
258 tab.setVisibility(View.VISIBLE);
259 //target.setVisibility(View.INVISIBLE);
260 if (animate) {
261 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
262 int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
263 int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
264
265 Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
266 trans.setDuration(ANIM_DURATION);
267 tab.startAnimation(trans);
268 text.startAnimation(trans);
269 }
270 }
271
Jim Miller24ccf3c2009-11-10 21:37:45 -0800272 void setState(int state) {
273 text.setPressed(state == STATE_PRESSED);
274 tab.setPressed(state == STATE_PRESSED);
275 if (state == STATE_ACTIVE) {
276 final int[] activeState = new int[] {com.android.internal.R.attr.state_active};
277 if (text.getBackground().isStateful()) {
278 text.getBackground().setState(activeState);
279 }
280 if (tab.getBackground().isStateful()) {
281 tab.getBackground().setState(activeState);
282 }
283 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
284 } else {
285 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
286 }
Jim Miller4df2c542009-11-12 03:39:14 -0800287 currentState = state;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800288 }
289
290 void showTarget() {
Jim Miller521d4002009-11-15 16:19:24 -0800291 AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
292 alphaAnim.setDuration(ANIM_TARGET_TIME);
293 target.startAnimation(alphaAnim);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800294 target.setVisibility(View.VISIBLE);
295 }
296
Jim Miller521d4002009-11-15 16:19:24 -0800297 void reset(boolean animate) {
Jim Miller24ccf3c2009-11-10 21:37:45 -0800298 setState(STATE_NORMAL);
299 text.setVisibility(View.VISIBLE);
300 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
301 tab.setVisibility(View.VISIBLE);
302 target.setVisibility(View.INVISIBLE);
Jim Miller521d4002009-11-15 16:19:24 -0800303 final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
304 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getLeft()
305 : alignment_value - tab.getRight()) : 0;
306 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
307 : alignment_value - tab.getBottom());
308 if (animate) {
309 TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
310 trans.setDuration(ANIM_DURATION);
311 trans.setFillAfter(false);
312 text.startAnimation(trans);
313 tab.startAnimation(trans);
314 } else {
315 if (horiz) {
316 text.offsetLeftAndRight(dx);
317 tab.offsetLeftAndRight(dx);
318 } else {
319 text.offsetTopAndBottom(dy);
320 tab.offsetTopAndBottom(dy);
321 }
Jim Miller4f01d4a2009-11-16 23:08:35 -0800322 text.clearAnimation();
323 tab.clearAnimation();
324 target.clearAnimation();
Jim Miller521d4002009-11-15 16:19:24 -0800325 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800326 }
327
328 void setTarget(int targetId) {
329 target.setImageResource(targetId);
330 }
331
332 /**
333 * Layout the given widgets within the parent.
334 *
335 * @param l the parent's left border
336 * @param t the parent's top border
337 * @param r the parent's right border
338 * @param b the parent's bottom border
339 * @param alignment which side to align the widget to
340 */
341 void layout(int l, int t, int r, int b, int alignment) {
Jim Miller521d4002009-11-15 16:19:24 -0800342 this.alignment = alignment;
Jim Miller753401a2009-11-11 17:53:53 -0800343 final Drawable tabBackground = tab.getBackground();
344 final int handleWidth = tabBackground.getIntrinsicWidth();
345 final int handleHeight = tabBackground.getIntrinsicHeight();
346 final Drawable targetDrawable = target.getDrawable();
347 final int targetWidth = targetDrawable.getIntrinsicWidth();
348 final int targetHeight = targetDrawable.getIntrinsicHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800349 final int parentWidth = r - l;
350 final int parentHeight = b - t;
351
Jim Miller4df2c542009-11-12 03:39:14 -0800352 final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
353 final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800354 final int left = (parentWidth - handleWidth) / 2;
355 final int right = left + handleWidth;
356
357 if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
358 // horizontal
359 final int targetTop = (parentHeight - targetHeight) / 2;
360 final int targetBottom = targetTop + targetHeight;
361 final int top = (parentHeight - handleHeight) / 2;
362 final int bottom = (parentHeight + handleHeight) / 2;
363 if (alignment == ALIGN_LEFT) {
364 tab.layout(0, top, handleWidth, bottom);
365 text.layout(0 - parentWidth, top, 0, bottom);
366 text.setGravity(Gravity.RIGHT);
367 target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
Jim Miller521d4002009-11-15 16:19:24 -0800368 alignment_value = l;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800369 } else {
370 tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
371 text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
372 target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
373 text.setGravity(Gravity.TOP);
Jim Miller521d4002009-11-15 16:19:24 -0800374 alignment_value = r;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800375 }
376 } else {
377 // vertical
378 final int targetLeft = (parentWidth - targetWidth) / 2;
379 final int targetRight = (parentWidth + targetWidth) / 2;
Jim Miller4df2c542009-11-12 03:39:14 -0800380 final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
381 final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800382 if (alignment == ALIGN_TOP) {
383 tab.layout(left, 0, right, handleHeight);
384 text.layout(left, 0 - parentHeight, right, 0);
385 target.layout(targetLeft, top, targetRight, top + targetHeight);
Jim Miller521d4002009-11-15 16:19:24 -0800386 alignment_value = t;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800387 } else {
388 tab.layout(left, parentHeight - handleHeight, right, parentHeight);
389 text.layout(left, parentHeight, right, parentHeight + parentHeight);
390 target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
Jim Miller521d4002009-11-15 16:19:24 -0800391 alignment_value = b;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800392 }
393 }
394 }
395
Jim Miller4df2c542009-11-12 03:39:14 -0800396 public void updateDrawableStates() {
397 setState(currentState);
398 }
399
Jim Miller4811d622009-11-12 17:45:07 -0800400 /**
401 * Ensure all the dependent widgets are measured.
402 */
403 public void measure() {
404 tab.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
405 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
406 text.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
407 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
Jim Miller24ccf3c2009-11-10 21:37:45 -0800408 }
409
Jim Miller4811d622009-11-12 17:45:07 -0800410 /**
411 * Get the measured tab width. Must be called after {@link Slider#measure()}.
412 * @return
413 */
414 public int getTabWidth() {
415 return tab.getMeasuredWidth();
416 }
417
418 /**
419 * Get the measured tab width. Must be called after {@link Slider#measure()}.
420 * @return
421 */
Jim Miller24ccf3c2009-11-10 21:37:45 -0800422 public int getTabHeight() {
Jim Miller4811d622009-11-12 17:45:07 -0800423 return tab.getMeasuredHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800424 }
Jim Miller521d4002009-11-15 16:19:24 -0800425
Jim Miller8b63ab62009-12-02 13:42:48 -0800426 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +0900427 * Start animating the slider. Note we need two animations since a ValueAnimator
Jim Miller8b63ab62009-12-02 13:42:48 -0800428 * keeps internal state of the invalidation region which is just the view being animated.
Jim Miller2cd1e6e2010-01-15 16:20:36 -0800429 *
Jim Miller8b63ab62009-12-02 13:42:48 -0800430 * @param anim1
431 * @param anim2
432 */
433 public void startAnimation(Animation anim1, Animation anim2) {
434 tab.startAnimation(anim1);
435 text.startAnimation(anim2);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800436 }
437
438 public void hideTarget() {
439 target.clearAnimation();
440 target.setVisibility(View.INVISIBLE);
Jim Miller521d4002009-11-15 16:19:24 -0800441 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800442 }
443
444 public SlidingTab(Context context) {
445 this(context, null);
446 }
447
448 /**
449 * Constructor used when this widget is created from a layout file.
450 */
451 public SlidingTab(Context context, AttributeSet attrs) {
452 super(context, attrs);
453
Jim Miller521d4002009-11-15 16:19:24 -0800454 // Allocate a temporary once that can be used everywhere.
455 mTmpRect = new Rect();
456
Jim Miller24ccf3c2009-11-10 21:37:45 -0800457 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
458 mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
459 a.recycle();
460
461 Resources r = getResources();
462 mDensity = r.getDisplayMetrics().density;
463 if (DBG) log("- Density: " + mDensity);
464
Jim Miller425ca592009-11-13 19:20:28 -0800465 mLeftSlider = new Slider(this,
466 R.drawable.jog_tab_left_generic,
Jim Miller24ccf3c2009-11-10 21:37:45 -0800467 R.drawable.jog_tab_bar_left_generic,
468 R.drawable.jog_tab_target_gray);
Jim Miller425ca592009-11-13 19:20:28 -0800469 mRightSlider = new Slider(this,
470 R.drawable.jog_tab_right_generic,
Jim Miller24ccf3c2009-11-10 21:37:45 -0800471 R.drawable.jog_tab_bar_right_generic,
472 R.drawable.jog_tab_target_gray);
473
474 // setBackgroundColor(0x80808080);
475 }
476
477 @Override
478 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
479 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
480 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
481
482 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
483 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
484
Jim Miller3747f3f2010-08-17 13:31:46 -0700485 if (DBG) {
486 if (widthSpecMode == MeasureSpec.UNSPECIFIED
487 || heightSpecMode == MeasureSpec.UNSPECIFIED) {
488 Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec"
489 +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")",
490 new RuntimeException(LOG_TAG + "stack:"));
491 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800492 }
493
Jim Miller4811d622009-11-12 17:45:07 -0800494 mLeftSlider.measure();
495 mRightSlider.measure();
496 final int leftTabWidth = mLeftSlider.getTabWidth();
497 final int rightTabWidth = mRightSlider.getTabWidth();
498 final int leftTabHeight = mLeftSlider.getTabHeight();
499 final int rightTabHeight = mRightSlider.getTabHeight();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800500 final int width;
501 final int height;
502 if (isHorizontal()) {
503 width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
504 height = Math.max(leftTabHeight, rightTabHeight);
505 } else {
506 width = Math.max(leftTabWidth, rightTabHeight);
507 height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
508 }
509 setMeasuredDimension(width, height);
510 }
511
512 @Override
513 public boolean onInterceptTouchEvent(MotionEvent event) {
514 final int action = event.getAction();
515 final float x = event.getX();
516 final float y = event.getY();
517
Jim Miller24ccf3c2009-11-10 21:37:45 -0800518 if (mAnimating) {
519 return false;
520 }
521
522 View leftHandle = mLeftSlider.tab;
Jim Miller521d4002009-11-15 16:19:24 -0800523 leftHandle.getHitRect(mTmpRect);
524 boolean leftHit = mTmpRect.contains((int) x, (int) y);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800525
526 View rightHandle = mRightSlider.tab;
Jim Miller521d4002009-11-15 16:19:24 -0800527 rightHandle.getHitRect(mTmpRect);
528 boolean rightHit = mTmpRect.contains((int)x, (int) y);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800529
530 if (!mTracking && !(leftHit || rightHit)) {
531 return false;
532 }
533
534 switch (action) {
535 case MotionEvent.ACTION_DOWN: {
536 mTracking = true;
537 mTriggered = false;
538 vibrate(VIBRATE_SHORT);
539 if (leftHit) {
540 mCurrentSlider = mLeftSlider;
541 mOtherSlider = mRightSlider;
Jim Miller4df2c542009-11-12 03:39:14 -0800542 mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800543 setGrabbedState(OnTriggerListener.LEFT_HANDLE);
544 } else {
545 mCurrentSlider = mRightSlider;
546 mOtherSlider = mLeftSlider;
Jim Miller4df2c542009-11-12 03:39:14 -0800547 mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
Jim Miller24ccf3c2009-11-10 21:37:45 -0800548 setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
549 }
550 mCurrentSlider.setState(Slider.STATE_PRESSED);
551 mCurrentSlider.showTarget();
552 mOtherSlider.hide();
553 break;
554 }
555 }
556
557 return true;
558 }
559
Jim Millerd8a3a892010-01-29 17:25:26 -0800560 /**
561 * Reset the tabs to their original state and stop any existing animation.
562 * Animate them back into place if animate is true.
563 *
564 * @param animate
565 */
566 public void reset(boolean animate) {
567 mLeftSlider.reset(animate);
568 mRightSlider.reset(animate);
Jim Miller966a8c72010-03-26 15:54:09 -0700569 if (!animate) {
570 mAnimating = false;
571 }
Jim Millerd8a3a892010-01-29 17:25:26 -0800572 }
573
Jim Miller24ccf3c2009-11-10 21:37:45 -0800574 @Override
Jim Miller4f01d4a2009-11-16 23:08:35 -0800575 public void setVisibility(int visibility) {
576 // Clear animations so sliders don't continue to animate when we show the widget again.
577 if (visibility != getVisibility() && visibility == View.INVISIBLE) {
Jim Millerd8a3a892010-01-29 17:25:26 -0800578 reset(false);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800579 }
580 super.setVisibility(visibility);
581 }
582
583 @Override
Jim Miller24ccf3c2009-11-10 21:37:45 -0800584 public boolean onTouchEvent(MotionEvent event) {
585 if (mTracking) {
586 final int action = event.getAction();
587 final float x = event.getX();
588 final float y = event.getY();
Jim Miller521d4002009-11-15 16:19:24 -0800589
Jim Miller24ccf3c2009-11-10 21:37:45 -0800590 switch (action) {
591 case MotionEvent.ACTION_MOVE:
Jim Miller521d4002009-11-15 16:19:24 -0800592 if (withinView(x, y, this) ) {
593 moveHandle(x, y);
594 float position = isHorizontal() ? x : y;
595 float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
596 boolean thresholdReached;
597 if (isHorizontal()) {
598 thresholdReached = mCurrentSlider == mLeftSlider ?
599 position > target : position < target;
600 } else {
601 thresholdReached = mCurrentSlider == mLeftSlider ?
602 position < target : position > target;
603 }
604 if (!mTriggered && thresholdReached) {
605 mTriggered = true;
606 mTracking = false;
607 mCurrentSlider.setState(Slider.STATE_ACTIVE);
Jim Miller4f01d4a2009-11-16 23:08:35 -0800608 boolean isLeft = mCurrentSlider == mLeftSlider;
609 dispatchTriggerEvent(isLeft ?
Jim Miller521d4002009-11-15 16:19:24 -0800610 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800611
Jim Miller4f01d4a2009-11-16 23:08:35 -0800612 startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
Jim Miller521d4002009-11-15 16:19:24 -0800613 setGrabbedState(OnTriggerListener.NO_HANDLE);
614 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800615 break;
616 }
617 // Intentionally fall through - we're outside tracking rectangle
618
619 case MotionEvent.ACTION_UP:
620 case MotionEvent.ACTION_CANCEL:
Jim Miller92c15712010-05-25 19:09:44 -0700621 cancelGrab();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800622 break;
623 }
624 }
625
626 return mTracking || super.onTouchEvent(event);
627 }
628
Jim Miller92c15712010-05-25 19:09:44 -0700629 private void cancelGrab() {
630 mTracking = false;
631 mTriggered = false;
632 mOtherSlider.show(true);
633 mCurrentSlider.reset(false);
634 mCurrentSlider.hideTarget();
635 mCurrentSlider = null;
636 mOtherSlider = null;
637 setGrabbedState(OnTriggerListener.NO_HANDLE);
638 }
639
Jim Miller4f01d4a2009-11-16 23:08:35 -0800640 void startAnimating(final boolean holdAfter) {
Jim Miller521d4002009-11-15 16:19:24 -0800641 mAnimating = true;
Jim Miller8b63ab62009-12-02 13:42:48 -0800642 final Animation trans1;
643 final Animation trans2;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800644 final Slider slider = mCurrentSlider;
645 final Slider other = mOtherSlider;
646 final int dx;
647 final int dy;
Jim Miller521d4002009-11-15 16:19:24 -0800648 if (isHorizontal()) {
649 int right = slider.tab.getRight();
650 int width = slider.tab.getWidth();
651 int left = slider.tab.getLeft();
652 int viewWidth = getWidth();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800653 int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
654 dx = slider == mRightSlider ? - (right + viewWidth - holdOffset)
655 : (viewWidth - left) + viewWidth - holdOffset;
Jim Miller521d4002009-11-15 16:19:24 -0800656 dy = 0;
657 } else {
658 int top = slider.tab.getTop();
659 int bottom = slider.tab.getBottom();
660 int height = slider.tab.getHeight();
661 int viewHeight = getHeight();
Jim Miller4f01d4a2009-11-16 23:08:35 -0800662 int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
Jim Miller521d4002009-11-15 16:19:24 -0800663 dx = 0;
Jim Miller4f01d4a2009-11-16 23:08:35 -0800664 dy = slider == mRightSlider ? (top + viewHeight - holdOffset)
665 : - ((viewHeight - bottom) + viewHeight - holdOffset);
Jim Miller521d4002009-11-15 16:19:24 -0800666 }
Jim Miller8b63ab62009-12-02 13:42:48 -0800667 trans1 = new TranslateAnimation(0, dx, 0, dy);
668 trans1.setDuration(ANIM_DURATION);
669 trans1.setInterpolator(new LinearInterpolator());
670 trans1.setFillAfter(true);
671 trans2 = new TranslateAnimation(0, dx, 0, dy);
672 trans2.setDuration(ANIM_DURATION);
673 trans2.setInterpolator(new LinearInterpolator());
674 trans2.setFillAfter(true);
Jim Miller521d4002009-11-15 16:19:24 -0800675
Jim Miller8b63ab62009-12-02 13:42:48 -0800676 trans1.setAnimationListener(new AnimationListener() {
Jim Miller521d4002009-11-15 16:19:24 -0800677 public void onAnimationEnd(Animation animation) {
Jim Miller4f01d4a2009-11-16 23:08:35 -0800678 Animation anim;
679 if (holdAfter) {
680 anim = new TranslateAnimation(dx, dx, dy, dy);
681 anim.setDuration(1000); // plenty of time for transitions
682 mAnimating = false;
683 } else {
684 anim = new AlphaAnimation(0.5f, 1.0f);
685 anim.setDuration(ANIM_DURATION);
686 resetView();
687 }
688 anim.setAnimationListener(mAnimationDoneListener);
Jim Miller2cd1e6e2010-01-15 16:20:36 -0800689
Jim Miller8b63ab62009-12-02 13:42:48 -0800690 /* Animation can be the same for these since the animation just holds */
691 mLeftSlider.startAnimation(anim, anim);
692 mRightSlider.startAnimation(anim, anim);
Jim Miller521d4002009-11-15 16:19:24 -0800693 }
694
695 public void onAnimationRepeat(Animation animation) {
696
697 }
698
699 public void onAnimationStart(Animation animation) {
700
701 }
702
703 });
704
Jim Miller4f01d4a2009-11-16 23:08:35 -0800705 slider.hideTarget();
Jim Miller8b63ab62009-12-02 13:42:48 -0800706 slider.startAnimation(trans1, trans2);
Jim Miller521d4002009-11-15 16:19:24 -0800707 }
708
Jim Miller4f01d4a2009-11-16 23:08:35 -0800709 private void onAnimationDone() {
710 resetView();
711 mAnimating = false;
712 }
713
Jim Miller521d4002009-11-15 16:19:24 -0800714 private boolean withinView(final float x, final float y, final View view) {
715 return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
716 || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
717 }
718
Jim Miller24ccf3c2009-11-10 21:37:45 -0800719 private boolean isHorizontal() {
720 return mOrientation == HORIZONTAL;
721 }
722
723 private void resetView() {
Jim Miller521d4002009-11-15 16:19:24 -0800724 mLeftSlider.reset(false);
725 mRightSlider.reset(false);
726 // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
Jim Miller24ccf3c2009-11-10 21:37:45 -0800727 }
728
729 @Override
730 protected void onLayout(boolean changed, int l, int t, int r, int b) {
731 if (!changed) return;
732
733 // Center the widgets in the view
734 mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
735 mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800736 }
737
738 private void moveHandle(float x, float y) {
739 final View handle = mCurrentSlider.tab;
740 final View content = mCurrentSlider.text;
741 if (isHorizontal()) {
742 int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
743 handle.offsetLeftAndRight(deltaX);
744 content.offsetLeftAndRight(deltaX);
745 } else {
746 int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
747 handle.offsetTopAndBottom(deltaY);
748 content.offsetTopAndBottom(deltaY);
749 }
750 invalidate(); // TODO: be more conservative about what we're invalidating
751 }
752
753 /**
754 * Sets the left handle icon to a given resource.
755 *
756 * The resource should refer to a Drawable object, or use 0 to remove
757 * the icon.
758 *
759 * @param iconId the resource ID of the icon drawable
760 * @param targetId the resource of the target drawable
761 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800762 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800763 */
764 public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
765 mLeftSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800766 mLeftSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800767 mLeftSlider.setBarBackgroundResource(barId);
768 mLeftSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800769 mLeftSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800770 }
771
772 /**
773 * Sets the left handle hint text to a given resource string.
774 *
775 * @param resId
776 */
777 public void setLeftHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800778 if (isHorizontal()) {
779 mLeftSlider.setHintText(resId);
780 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800781 }
782
783 /**
784 * Sets the right handle icon to a given resource.
785 *
786 * The resource should refer to a Drawable object, or use 0 to remove
787 * the icon.
788 *
789 * @param iconId the resource ID of the icon drawable
790 * @param targetId the resource of the target drawable
791 * @param barId the resource of the bar drawable (stateful)
Jim Miller425ca592009-11-13 19:20:28 -0800792 * @param tabId the resource of the
Jim Miller24ccf3c2009-11-10 21:37:45 -0800793 */
794 public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
795 mRightSlider.setIcon(iconId);
Jim Miller425ca592009-11-13 19:20:28 -0800796 mRightSlider.setTarget(targetId);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800797 mRightSlider.setBarBackgroundResource(barId);
798 mRightSlider.setTabBackgroundResource(tabId);
Jim Miller4df2c542009-11-12 03:39:14 -0800799 mRightSlider.updateDrawableStates();
Jim Miller24ccf3c2009-11-10 21:37:45 -0800800 }
801
802 /**
803 * Sets the left handle hint text to a given resource string.
804 *
805 * @param resId
806 */
807 public void setRightHintText(int resId) {
Jim Miller425ca592009-11-13 19:20:28 -0800808 if (isHorizontal()) {
809 mRightSlider.setHintText(resId);
810 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800811 }
812
Jim Miller4f01d4a2009-11-16 23:08:35 -0800813 public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
814 mHoldLeftOnTransition = holdLeft;
815 mHoldRightOnTransition = holdRight;
816 }
817
Jim Miller24ccf3c2009-11-10 21:37:45 -0800818 /**
819 * Triggers haptic feedback.
820 */
821 private synchronized void vibrate(long duration) {
Jeff Sharkey723a7252012-10-12 14:26:31 -0700822 final boolean hapticEnabled = Settings.System.getIntForUser(
823 mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
824 UserHandle.USER_CURRENT) != 0;
825 if (hapticEnabled) {
826 if (mVibrator == null) {
827 mVibrator = (android.os.Vibrator) getContext()
828 .getSystemService(Context.VIBRATOR_SERVICE);
829 }
John Spurlock7b414672014-07-18 13:02:39 -0400830 mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES);
Jim Miller24ccf3c2009-11-10 21:37:45 -0800831 }
Jim Miller24ccf3c2009-11-10 21:37:45 -0800832 }
833
834 /**
835 * Registers a callback to be invoked when the user triggers an event.
836 *
837 * @param listener the OnDialTriggerListener to attach to this view
838 */
839 public void setOnTriggerListener(OnTriggerListener listener) {
840 mOnTriggerListener = listener;
841 }
842
843 /**
844 * Dispatches a trigger event to listener. Ignored if a listener is not set.
845 * @param whichHandle the handle that triggered the event.
846 */
847 private void dispatchTriggerEvent(int whichHandle) {
848 vibrate(VIBRATE_LONG);
849 if (mOnTriggerListener != null) {
850 mOnTriggerListener.onTrigger(this, whichHandle);
851 }
852 }
853
Jim Miller92c15712010-05-25 19:09:44 -0700854 @Override
855 protected void onVisibilityChanged(View changedView, int visibility) {
856 super.onVisibilityChanged(changedView, visibility);
857 // When visibility changes and the user has a tab selected, unselect it and
858 // make sure their callback gets called.
859 if (changedView == this && visibility != VISIBLE
860 && mGrabbedState != OnTriggerListener.NO_HANDLE) {
861 cancelGrab();
862 }
863 }
864
Jim Miller24ccf3c2009-11-10 21:37:45 -0800865 /**
866 * Sets the current grabbed state, and dispatches a grabbed state change
867 * event to our listener.
868 */
869 private void setGrabbedState(int newState) {
870 if (newState != mGrabbedState) {
871 mGrabbedState = newState;
872 if (mOnTriggerListener != null) {
873 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
874 }
875 }
876 }
877
Jim Miller24ccf3c2009-11-10 21:37:45 -0800878 private void log(String msg) {
879 Log.d(LOG_TAG, msg);
880 }
881}