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