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