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