1 /* 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 17 package com.android.internal.widget; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Rect; 23 import android.graphics.drawable.Drawable; 24 import android.os.Vibrator; 25 import android.util.AttributeSet; 26 import android.util.Log; 27 import android.view.Gravity; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.animation.AlphaAnimation; 32 import android.view.animation.Animation; 33 import android.view.animation.LinearInterpolator; 34 import android.view.animation.TranslateAnimation; 35 import android.view.animation.Animation.AnimationListener; 36 import android.widget.ImageView; 37 import android.widget.TextView; 38 import android.widget.ImageView.ScaleType; 39 40 import com.android.internal.R; 41 42 /** 43 * A special widget containing two Sliders and a threshold for each. Moving either slider beyond 44 * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with 45 * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE} 46 * Equivalently, selecting a tab will result in a call to 47 * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing 48 * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}. 49 * 50 */ 51 public class SlidingTab extends ViewGroup { 52 private static final String LOG_TAG = "SlidingTab"; 53 private static final boolean DBG = false; 54 private static final int HORIZONTAL = 0; // as defined in attrs.xml 55 private static final int VERTICAL = 1; 56 57 // TODO: Make these configurable 58 private static final float THRESHOLD = 2.0f / 3.0f; 59 private static final long VIBRATE_SHORT = 30; 60 private static final long VIBRATE_LONG = 40; 61 private static final int TRACKING_MARGIN = 50; 62 private static final int ANIM_DURATION = 250; // Time for most animations (in ms) 63 private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms) 64 private boolean mHoldLeftOnTransition = true; 65 private boolean mHoldRightOnTransition = true; 66 67 private OnTriggerListener mOnTriggerListener; 68 private int mGrabbedState = OnTriggerListener.NO_HANDLE; 69 private boolean mTriggered = false; 70 private Vibrator mVibrator; 71 private final float mDensity; // used to scale dimensions for bitmaps. 72 73 /** 74 * Either {@link #HORIZONTAL} or {@link #VERTICAL}. 75 */ 76 private final int mOrientation; 77 78 private final Slider mLeftSlider; 79 private final Slider mRightSlider; 80 private Slider mCurrentSlider; 81 private boolean mTracking; 82 private float mThreshold; 83 private Slider mOtherSlider; 84 private boolean mAnimating; 85 private final Rect mTmpRect; 86 87 /** 88 * Listener used to reset the view when the current animation completes. 89 */ 90 private final AnimationListener mAnimationDoneListener = new AnimationListener() { 91 public void onAnimationStart(Animation animation) { 92 93 } 94 95 public void onAnimationRepeat(Animation animation) { 96 97 } 98 99 public void onAnimationEnd(Animation animation) { 100 onAnimationDone(); 101 } 102 }; 103 104 /** 105 * Interface definition for a callback to be invoked when a tab is triggered 106 * by moving it beyond a threshold. 107 */ 108 public interface OnTriggerListener { 109 /** 110 * The interface was triggered because the user let go of the handle without reaching the 111 * threshold. 112 */ 113 public static final int NO_HANDLE = 0; 114 115 /** 116 * The interface was triggered because the user grabbed the left handle and moved it past 117 * the threshold. 118 */ 119 public static final int LEFT_HANDLE = 1; 120 121 /** 122 * The interface was triggered because the user grabbed the right handle and moved it past 123 * the threshold. 124 */ 125 public static final int RIGHT_HANDLE = 2; 126 127 /** 128 * Called when the user moves a handle beyond the threshold. 129 * 130 * @param v The view that was triggered. 131 * @param whichHandle Which "dial handle" the user grabbed, 132 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}. 133 */ 134 void onTrigger(View v, int whichHandle); 135 136 /** 137 * Called when the "grabbed state" changes (i.e. when the user either grabs or releases 138 * one of the handles.) 139 * 140 * @param v the view that was triggered 141 * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE}, 142 * or {@link #RIGHT_HANDLE}. 143 */ 144 void onGrabbedStateChange(View v, int grabbedState); 145 } 146 147 /** 148 * Simple container class for all things pertinent to a slider. 149 * A slider consists of 3 Views: 150 * 151 * {@link #tab} is the tab shown on the screen in the default state. 152 * {@link #text} is the view revealed as the user slides the tab out. 153 * {@link #target} is the target the user must drag the slider past to trigger the slider. 154 * 155 */ 156 private static class Slider { 157 /** 158 * Tab alignment - determines which side the tab should be drawn on 159 */ 160 public static final int ALIGN_LEFT = 0; 161 public static final int ALIGN_RIGHT = 1; 162 public static final int ALIGN_TOP = 2; 163 public static final int ALIGN_BOTTOM = 3; 164 public static final int ALIGN_UNKNOWN = 4; 165 166 /** 167 * States for the view. 168 */ 169 private static final int STATE_NORMAL = 0; 170 private static final int STATE_PRESSED = 1; 171 private static final int STATE_ACTIVE = 2; 172 173 private final ImageView tab; 174 private final TextView text; 175 private final ImageView target; 176 private int currentState = STATE_NORMAL; 177 private int alignment = ALIGN_UNKNOWN; 178 private int alignment_value; 179 180 /** 181 * Constructor 182 * 183 * @param parent the container view of this one 184 * @param tabId drawable for the tab 185 * @param barId drawable for the bar 186 * @param targetId drawable for the target 187 */ 188 Slider(ViewGroup parent, int tabId, int barId, int targetId) { 189 // Create tab 190 tab = new ImageView(parent.getContext()); 191 tab.setBackgroundResource(tabId); 192 tab.setScaleType(ScaleType.CENTER); 193 tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, 194 LayoutParams.WRAP_CONTENT)); 195 196 // Create hint TextView 197 text = new TextView(parent.getContext()); 198 text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, 199 LayoutParams.MATCH_PARENT)); 200 text.setBackgroundResource(barId); 201 text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal); 202 // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen 203 204 // Create target 205 target = new ImageView(parent.getContext()); 206 target.setImageResource(targetId); 207 target.setScaleType(ScaleType.CENTER); 208 target.setLayoutParams( 209 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); 210 target.setVisibility(View.INVISIBLE); 211 212 parent.addView(target); // this needs to be first - relies on painter's algorithm 213 parent.addView(tab); 214 parent.addView(text); 215 } 216 217 void setIcon(int iconId) { 218 tab.setImageResource(iconId); 219 } 220 221 void setTabBackgroundResource(int tabId) { 222 tab.setBackgroundResource(tabId); 223 } 224 225 void setBarBackgroundResource(int barId) { 226 text.setBackgroundResource(barId); 227 } 228 229 void setHintText(int resId) { 230 text.setText(resId); 231 } 232 233 void hide() { 234 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT; 235 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight() 236 : alignment_value - tab.getLeft()) : 0; 237 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom() 238 : alignment_value - tab.getTop()); 239 240 Animation trans = new TranslateAnimation(0, dx, 0, dy); 241 trans.setDuration(ANIM_DURATION); 242 trans.setFillAfter(true); 243 tab.startAnimation(trans); 244 text.startAnimation(trans); 245 target.setVisibility(View.INVISIBLE); 246 } 247 248 void show(boolean animate) { 249 text.setVisibility(View.VISIBLE); 250 tab.setVisibility(View.VISIBLE); 251 //target.setVisibility(View.INVISIBLE); 252 if (animate) { 253 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT; 254 int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0; 255 int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight()); 256 257 Animation trans = new TranslateAnimation(-dx, 0, -dy, 0); 258 trans.setDuration(ANIM_DURATION); 259 tab.startAnimation(trans); 260 text.startAnimation(trans); 261 } 262 } 263 264 void setState(int state) { 265 text.setPressed(state == STATE_PRESSED); 266 tab.setPressed(state == STATE_PRESSED); 267 if (state == STATE_ACTIVE) { 268 final int[] activeState = new int[] {com.android.internal.R.attr.state_active}; 269 if (text.getBackground().isStateful()) { 270 text.getBackground().setState(activeState); 271 } 272 if (tab.getBackground().isStateful()) { 273 tab.getBackground().setState(activeState); 274 } 275 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive); 276 } else { 277 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal); 278 } 279 currentState = state; 280 } 281 282 void showTarget() { 283 AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f); 284 alphaAnim.setDuration(ANIM_TARGET_TIME); 285 target.startAnimation(alphaAnim); 286 target.setVisibility(View.VISIBLE); 287 } 288 289 void reset(boolean animate) { 290 setState(STATE_NORMAL); 291 text.setVisibility(View.VISIBLE); 292 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal); 293 tab.setVisibility(View.VISIBLE); 294 target.setVisibility(View.INVISIBLE); 295 final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT; 296 int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getLeft() 297 : alignment_value - tab.getRight()) : 0; 298 int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop() 299 : alignment_value - tab.getBottom()); 300 if (animate) { 301 TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy); 302 trans.setDuration(ANIM_DURATION); 303 trans.setFillAfter(false); 304 text.startAnimation(trans); 305 tab.startAnimation(trans); 306 } else { 307 if (horiz) { 308 text.offsetLeftAndRight(dx); 309 tab.offsetLeftAndRight(dx); 310 } else { 311 text.offsetTopAndBottom(dy); 312 tab.offsetTopAndBottom(dy); 313 } 314 text.clearAnimation(); 315 tab.clearAnimation(); 316 target.clearAnimation(); 317 } 318 } 319 320 void setTarget(int targetId) { 321 target.setImageResource(targetId); 322 } 323 324 /** 325 * Layout the given widgets within the parent. 326 * 327 * @param l the parent's left border 328 * @param t the parent's top border 329 * @param r the parent's right border 330 * @param b the parent's bottom border 331 * @param alignment which side to align the widget to 332 */ 333 void layout(int l, int t, int r, int b, int alignment) { 334 this.alignment = alignment; 335 final Drawable tabBackground = tab.getBackground(); 336 final int handleWidth = tabBackground.getIntrinsicWidth(); 337 final int handleHeight = tabBackground.getIntrinsicHeight(); 338 final Drawable targetDrawable = target.getDrawable(); 339 final int targetWidth = targetDrawable.getIntrinsicWidth(); 340 final int targetHeight = targetDrawable.getIntrinsicHeight(); 341 final int parentWidth = r - l; 342 final int parentHeight = b - t; 343 344 final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2; 345 final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2; 346 final int left = (parentWidth - handleWidth) / 2; 347 final int right = left + handleWidth; 348 349 if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) { 350 // horizontal 351 final int targetTop = (parentHeight - targetHeight) / 2; 352 final int targetBottom = targetTop + targetHeight; 353 final int top = (parentHeight - handleHeight) / 2; 354 final int bottom = (parentHeight + handleHeight) / 2; 355 if (alignment == ALIGN_LEFT) { 356 tab.layout(0, top, handleWidth, bottom); 357 text.layout(0 - parentWidth, top, 0, bottom); 358 text.setGravity(Gravity.RIGHT); 359 target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom); 360 alignment_value = l; 361 } else { 362 tab.layout(parentWidth - handleWidth, top, parentWidth, bottom); 363 text.layout(parentWidth, top, parentWidth + parentWidth, bottom); 364 target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom); 365 text.setGravity(Gravity.TOP); 366 alignment_value = r; 367 } 368 } else { 369 // vertical 370 final int targetLeft = (parentWidth - targetWidth) / 2; 371 final int targetRight = (parentWidth + targetWidth) / 2; 372 final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight; 373 final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2; 374 if (alignment == ALIGN_TOP) { 375 tab.layout(left, 0, right, handleHeight); 376 text.layout(left, 0 - parentHeight, right, 0); 377 target.layout(targetLeft, top, targetRight, top + targetHeight); 378 alignment_value = t; 379 } else { 380 tab.layout(left, parentHeight - handleHeight, right, parentHeight); 381 text.layout(left, parentHeight, right, parentHeight + parentHeight); 382 target.layout(targetLeft, bottom, targetRight, bottom + targetHeight); 383 alignment_value = b; 384 } 385 } 386 } 387 388 public void updateDrawableStates() { 389 setState(currentState); 390 } 391 392 /** 393 * Ensure all the dependent widgets are measured. 394 */ 395 public void measure() { 396 tab.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), 397 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); 398 text.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), 399 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); 400 } 401 402 /** 403 * Get the measured tab width. Must be called after {@link Slider#measure()}. 404 * @return 405 */ 406 public int getTabWidth() { 407 return tab.getMeasuredWidth(); 408 } 409 410 /** 411 * Get the measured tab width. Must be called after {@link Slider#measure()}. 412 * @return 413 */ 414 public int getTabHeight() { 415 return tab.getMeasuredHeight(); 416 } 417 418 /** 419 * Start animating the slider. Note we need two animations since a ValueAnimator 420 * keeps internal state of the invalidation region which is just the view being animated. 421 * 422 * @param anim1 423 * @param anim2 424 */ 425 public void startAnimation(Animation anim1, Animation anim2) { 426 tab.startAnimation(anim1); 427 text.startAnimation(anim2); 428 } 429 430 public void hideTarget() { 431 target.clearAnimation(); 432 target.setVisibility(View.INVISIBLE); 433 } 434 } 435 436 public SlidingTab(Context context) { 437 this(context, null); 438 } 439 440 /** 441 * Constructor used when this widget is created from a layout file. 442 */ 443 public SlidingTab(Context context, AttributeSet attrs) { 444 super(context, attrs); 445 446 // Allocate a temporary once that can be used everywhere. 447 mTmpRect = new Rect(); 448 449 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab); 450 mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL); 451 a.recycle(); 452 453 Resources r = getResources(); 454 mDensity = r.getDisplayMetrics().density; 455 if (DBG) log("- Density: " + mDensity); 456 457 mLeftSlider = new Slider(this, 458 R.drawable.jog_tab_left_generic, 459 R.drawable.jog_tab_bar_left_generic, 460 R.drawable.jog_tab_target_gray); 461 mRightSlider = new Slider(this, 462 R.drawable.jog_tab_right_generic, 463 R.drawable.jog_tab_bar_right_generic, 464 R.drawable.jog_tab_target_gray); 465 466 // setBackgroundColor(0x80808080); 467 } 468 469 @Override 470 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 471 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 472 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 473 474 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 475 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 476 477 if (DBG) { 478 if (widthSpecMode == MeasureSpec.UNSPECIFIED 479 || heightSpecMode == MeasureSpec.UNSPECIFIED) { 480 Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec" 481 +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")", 482 new RuntimeException(LOG_TAG + "stack:")); 483 } 484 } 485 486 mLeftSlider.measure(); 487 mRightSlider.measure(); 488 final int leftTabWidth = mLeftSlider.getTabWidth(); 489 final int rightTabWidth = mRightSlider.getTabWidth(); 490 final int leftTabHeight = mLeftSlider.getTabHeight(); 491 final int rightTabHeight = mRightSlider.getTabHeight(); 492 final int width; 493 final int height; 494 if (isHorizontal()) { 495 width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth); 496 height = Math.max(leftTabHeight, rightTabHeight); 497 } else { 498 width = Math.max(leftTabWidth, rightTabHeight); 499 height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight); 500 } 501 setMeasuredDimension(width, height); 502 } 503 504 @Override 505 public boolean onInterceptTouchEvent(MotionEvent event) { 506 final int action = event.getAction(); 507 final float x = event.getX(); 508 final float y = event.getY(); 509 510 if (mAnimating) { 511 return false; 512 } 513 514 View leftHandle = mLeftSlider.tab; 515 leftHandle.getHitRect(mTmpRect); 516 boolean leftHit = mTmpRect.contains((int) x, (int) y); 517 518 View rightHandle = mRightSlider.tab; 519 rightHandle.getHitRect(mTmpRect); 520 boolean rightHit = mTmpRect.contains((int)x, (int) y); 521 522 if (!mTracking && !(leftHit || rightHit)) { 523 return false; 524 } 525 526 switch (action) { 527 case MotionEvent.ACTION_DOWN: { 528 mTracking = true; 529 mTriggered = false; 530 vibrate(VIBRATE_SHORT); 531 if (leftHit) { 532 mCurrentSlider = mLeftSlider; 533 mOtherSlider = mRightSlider; 534 mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD; 535 setGrabbedState(OnTriggerListener.LEFT_HANDLE); 536 } else { 537 mCurrentSlider = mRightSlider; 538 mOtherSlider = mLeftSlider; 539 mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD; 540 setGrabbedState(OnTriggerListener.RIGHT_HANDLE); 541 } 542 mCurrentSlider.setState(Slider.STATE_PRESSED); 543 mCurrentSlider.showTarget(); 544 mOtherSlider.hide(); 545 break; 546 } 547 } 548 549 return true; 550 } 551 552 /** 553 * Reset the tabs to their original state and stop any existing animation. 554 * Animate them back into place if animate is true. 555 * 556 * @param animate 557 */ 558 public void reset(boolean animate) { 559 mLeftSlider.reset(animate); 560 mRightSlider.reset(animate); 561 if (!animate) { 562 mAnimating = false; 563 } 564 } 565 566 @Override 567 public void setVisibility(int visibility) { 568 // Clear animations so sliders don't continue to animate when we show the widget again. 569 if (visibility != getVisibility() && visibility == View.INVISIBLE) { 570 reset(false); 571 } 572 super.setVisibility(visibility); 573 } 574 575 @Override 576 public boolean onTouchEvent(MotionEvent event) { 577 if (mTracking) { 578 final int action = event.getAction(); 579 final float x = event.getX(); 580 final float y = event.getY(); 581 582 switch (action) { 583 case MotionEvent.ACTION_MOVE: 584 if (withinView(x, y, this) ) { 585 moveHandle(x, y); 586 float position = isHorizontal() ? x : y; 587 float target = mThreshold * (isHorizontal() ? getWidth() : getHeight()); 588 boolean thresholdReached; 589 if (isHorizontal()) { 590 thresholdReached = mCurrentSlider == mLeftSlider ? 591 position > target : position < target; 592 } else { 593 thresholdReached = mCurrentSlider == mLeftSlider ? 594 position < target : position > target; 595 } 596 if (!mTriggered && thresholdReached) { 597 mTriggered = true; 598 mTracking = false; 599 mCurrentSlider.setState(Slider.STATE_ACTIVE); 600 boolean isLeft = mCurrentSlider == mLeftSlider; 601 dispatchTriggerEvent(isLeft ? 602 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE); 603 604 startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition); 605 setGrabbedState(OnTriggerListener.NO_HANDLE); 606 } 607 break; 608 } 609 // Intentionally fall through - we're outside tracking rectangle 610 611 case MotionEvent.ACTION_UP: 612 case MotionEvent.ACTION_CANCEL: 613 cancelGrab(); 614 break; 615 } 616 } 617 618 return mTracking || super.onTouchEvent(event); 619 } 620 621 private void cancelGrab() { 622 mTracking = false; 623 mTriggered = false; 624 mOtherSlider.show(true); 625 mCurrentSlider.reset(false); 626 mCurrentSlider.hideTarget(); 627 mCurrentSlider = null; 628 mOtherSlider = null; 629 setGrabbedState(OnTriggerListener.NO_HANDLE); 630 } 631 632 void startAnimating(final boolean holdAfter) { 633 mAnimating = true; 634 final Animation trans1; 635 final Animation trans2; 636 final Slider slider = mCurrentSlider; 637 final Slider other = mOtherSlider; 638 final int dx; 639 final int dy; 640 if (isHorizontal()) { 641 int right = slider.tab.getRight(); 642 int width = slider.tab.getWidth(); 643 int left = slider.tab.getLeft(); 644 int viewWidth = getWidth(); 645 int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim 646 dx = slider == mRightSlider ? - (right + viewWidth - holdOffset) 647 : (viewWidth - left) + viewWidth - holdOffset; 648 dy = 0; 649 } else { 650 int top = slider.tab.getTop(); 651 int bottom = slider.tab.getBottom(); 652 int height = slider.tab.getHeight(); 653 int viewHeight = getHeight(); 654 int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim 655 dx = 0; 656 dy = slider == mRightSlider ? (top + viewHeight - holdOffset) 657 : - ((viewHeight - bottom) + viewHeight - holdOffset); 658 } 659 trans1 = new TranslateAnimation(0, dx, 0, dy); 660 trans1.setDuration(ANIM_DURATION); 661 trans1.setInterpolator(new LinearInterpolator()); 662 trans1.setFillAfter(true); 663 trans2 = new TranslateAnimation(0, dx, 0, dy); 664 trans2.setDuration(ANIM_DURATION); 665 trans2.setInterpolator(new LinearInterpolator()); 666 trans2.setFillAfter(true); 667 668 trans1.setAnimationListener(new AnimationListener() { 669 public void onAnimationEnd(Animation animation) { 670 Animation anim; 671 if (holdAfter) { 672 anim = new TranslateAnimation(dx, dx, dy, dy); 673 anim.setDuration(1000); // plenty of time for transitions 674 mAnimating = false; 675 } else { 676 anim = new AlphaAnimation(0.5f, 1.0f); 677 anim.setDuration(ANIM_DURATION); 678 resetView(); 679 } 680 anim.setAnimationListener(mAnimationDoneListener); 681 682 /* Animation can be the same for these since the animation just holds */ 683 mLeftSlider.startAnimation(anim, anim); 684 mRightSlider.startAnimation(anim, anim); 685 } 686 687 public void onAnimationRepeat(Animation animation) { 688 689 } 690 691 public void onAnimationStart(Animation animation) { 692 693 } 694 695 }); 696 697 slider.hideTarget(); 698 slider.startAnimation(trans1, trans2); 699 } 700 701 private void onAnimationDone() { 702 resetView(); 703 mAnimating = false; 704 } 705 706 private boolean withinView(final float x, final float y, final View view) { 707 return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight() 708 || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth(); 709 } 710 711 private boolean isHorizontal() { 712 return mOrientation == HORIZONTAL; 713 } 714 715 private void resetView() { 716 mLeftSlider.reset(false); 717 mRightSlider.reset(false); 718 // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight()); 719 } 720 721 @Override 722 protected void onLayout(boolean changed, int l, int t, int r, int b) { 723 if (!changed) return; 724 725 // Center the widgets in the view 726 mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM); 727 mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP); 728 } 729 730 private void moveHandle(float x, float y) { 731 final View handle = mCurrentSlider.tab; 732 final View content = mCurrentSlider.text; 733 if (isHorizontal()) { 734 int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2); 735 handle.offsetLeftAndRight(deltaX); 736 content.offsetLeftAndRight(deltaX); 737 } else { 738 int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2); 739 handle.offsetTopAndBottom(deltaY); 740 content.offsetTopAndBottom(deltaY); 741 } 742 invalidate(); // TODO: be more conservative about what we're invalidating 743 } 744 745 /** 746 * Sets the left handle icon to a given resource. 747 * 748 * The resource should refer to a Drawable object, or use 0 to remove 749 * the icon. 750 * 751 * @param iconId the resource ID of the icon drawable 752 * @param targetId the resource of the target drawable 753 * @param barId the resource of the bar drawable (stateful) 754 * @param tabId the resource of the 755 */ 756 public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) { 757 mLeftSlider.setIcon(iconId); 758 mLeftSlider.setTarget(targetId); 759 mLeftSlider.setBarBackgroundResource(barId); 760 mLeftSlider.setTabBackgroundResource(tabId); 761 mLeftSlider.updateDrawableStates(); 762 } 763 764 /** 765 * Sets the left handle hint text to a given resource string. 766 * 767 * @param resId 768 */ 769 public void setLeftHintText(int resId) { 770 if (isHorizontal()) { 771 mLeftSlider.setHintText(resId); 772 } 773 } 774 775 /** 776 * Sets the right handle icon to a given resource. 777 * 778 * The resource should refer to a Drawable object, or use 0 to remove 779 * the icon. 780 * 781 * @param iconId the resource ID of the icon drawable 782 * @param targetId the resource of the target drawable 783 * @param barId the resource of the bar drawable (stateful) 784 * @param tabId the resource of the 785 */ 786 public void setRightTabResources(int iconId, int targetId, int barId, int tabId) { 787 mRightSlider.setIcon(iconId); 788 mRightSlider.setTarget(targetId); 789 mRightSlider.setBarBackgroundResource(barId); 790 mRightSlider.setTabBackgroundResource(tabId); 791 mRightSlider.updateDrawableStates(); 792 } 793 794 /** 795 * Sets the left handle hint text to a given resource string. 796 * 797 * @param resId 798 */ 799 public void setRightHintText(int resId) { 800 if (isHorizontal()) { 801 mRightSlider.setHintText(resId); 802 } 803 } 804 805 public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) { 806 mHoldLeftOnTransition = holdLeft; 807 mHoldRightOnTransition = holdRight; 808 } 809 810 /** 811 * Triggers haptic feedback. 812 */ 813 private synchronized void vibrate(long duration) { 814 if (mVibrator == null) { 815 mVibrator = (android.os.Vibrator) 816 getContext().getSystemService(Context.VIBRATOR_SERVICE); 817 } 818 mVibrator.vibrate(duration); 819 } 820 821 /** 822 * Registers a callback to be invoked when the user triggers an event. 823 * 824 * @param listener the OnDialTriggerListener to attach to this view 825 */ 826 public void setOnTriggerListener(OnTriggerListener listener) { 827 mOnTriggerListener = listener; 828 } 829 830 /** 831 * Dispatches a trigger event to listener. Ignored if a listener is not set. 832 * @param whichHandle the handle that triggered the event. 833 */ 834 private void dispatchTriggerEvent(int whichHandle) { 835 vibrate(VIBRATE_LONG); 836 if (mOnTriggerListener != null) { 837 mOnTriggerListener.onTrigger(this, whichHandle); 838 } 839 } 840 841 @Override 842 protected void onVisibilityChanged(View changedView, int visibility) { 843 super.onVisibilityChanged(changedView, visibility); 844 // When visibility changes and the user has a tab selected, unselect it and 845 // make sure their callback gets called. 846 if (changedView == this && visibility != VISIBLE 847 && mGrabbedState != OnTriggerListener.NO_HANDLE) { 848 cancelGrab(); 849 } 850 } 851 852 /** 853 * Sets the current grabbed state, and dispatches a grabbed state change 854 * event to our listener. 855 */ 856 private void setGrabbedState(int newState) { 857 if (newState != mGrabbedState) { 858 mGrabbedState = newState; 859 if (mOnTriggerListener != null) { 860 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState); 861 } 862 } 863 } 864 865 private void log(String msg) { 866 Log.d(LOG_TAG, msg); 867 } 868 } 869