SlidingTab.java revision 2cd1e6eda90170114e0795b13f65f964296cf2f2
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 @Override 552 public void setVisibility(int visibility) { 553 // Clear animations so sliders don't continue to animate when we show the widget again. 554 if (visibility != getVisibility() && visibility == View.INVISIBLE) { 555 mLeftSlider.reset(false); 556 mRightSlider.reset(false); 557 } 558 super.setVisibility(visibility); 559 } 560 561 @Override 562 public boolean onTouchEvent(MotionEvent event) { 563 if (mTracking) { 564 final int action = event.getAction(); 565 final float x = event.getX(); 566 final float y = event.getY(); 567 568 switch (action) { 569 case MotionEvent.ACTION_MOVE: 570 if (withinView(x, y, this) ) { 571 moveHandle(x, y); 572 float position = isHorizontal() ? x : y; 573 float target = mThreshold * (isHorizontal() ? getWidth() : getHeight()); 574 boolean thresholdReached; 575 if (isHorizontal()) { 576 thresholdReached = mCurrentSlider == mLeftSlider ? 577 position > target : position < target; 578 } else { 579 thresholdReached = mCurrentSlider == mLeftSlider ? 580 position < target : position > target; 581 } 582 if (!mTriggered && thresholdReached) { 583 mTriggered = true; 584 mTracking = false; 585 mCurrentSlider.setState(Slider.STATE_ACTIVE); 586 boolean isLeft = mCurrentSlider == mLeftSlider; 587 dispatchTriggerEvent(isLeft ? 588 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE); 589 590 startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition); 591 setGrabbedState(OnTriggerListener.NO_HANDLE); 592 } 593 break; 594 } 595 // Intentionally fall through - we're outside tracking rectangle 596 597 case MotionEvent.ACTION_UP: 598 case MotionEvent.ACTION_CANCEL: 599 mTracking = false; 600 mTriggered = false; 601 mOtherSlider.show(true); 602 mCurrentSlider.reset(false); 603 mCurrentSlider.hideTarget(); 604 mCurrentSlider = null; 605 mOtherSlider = null; 606 setGrabbedState(OnTriggerListener.NO_HANDLE); 607 break; 608 } 609 } 610 611 return mTracking || super.onTouchEvent(event); 612 } 613 614 void startAnimating(final boolean holdAfter) { 615 mAnimating = true; 616 final Animation trans1; 617 final Animation trans2; 618 final Slider slider = mCurrentSlider; 619 final Slider other = mOtherSlider; 620 final int dx; 621 final int dy; 622 if (isHorizontal()) { 623 int right = slider.tab.getRight(); 624 int width = slider.tab.getWidth(); 625 int left = slider.tab.getLeft(); 626 int viewWidth = getWidth(); 627 int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim 628 dx = slider == mRightSlider ? - (right + viewWidth - holdOffset) 629 : (viewWidth - left) + viewWidth - holdOffset; 630 dy = 0; 631 } else { 632 int top = slider.tab.getTop(); 633 int bottom = slider.tab.getBottom(); 634 int height = slider.tab.getHeight(); 635 int viewHeight = getHeight(); 636 int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim 637 dx = 0; 638 dy = slider == mRightSlider ? (top + viewHeight - holdOffset) 639 : - ((viewHeight - bottom) + viewHeight - holdOffset); 640 } 641 trans1 = new TranslateAnimation(0, dx, 0, dy); 642 trans1.setDuration(ANIM_DURATION); 643 trans1.setInterpolator(new LinearInterpolator()); 644 trans1.setFillAfter(true); 645 trans2 = new TranslateAnimation(0, dx, 0, dy); 646 trans2.setDuration(ANIM_DURATION); 647 trans2.setInterpolator(new LinearInterpolator()); 648 trans2.setFillAfter(true); 649 650 trans1.setAnimationListener(new AnimationListener() { 651 public void onAnimationEnd(Animation animation) { 652 Animation anim; 653 if (holdAfter) { 654 anim = new TranslateAnimation(dx, dx, dy, dy); 655 anim.setDuration(1000); // plenty of time for transitions 656 mAnimating = false; 657 } else { 658 anim = new AlphaAnimation(0.5f, 1.0f); 659 anim.setDuration(ANIM_DURATION); 660 resetView(); 661 } 662 anim.setAnimationListener(mAnimationDoneListener); 663 664 /* Animation can be the same for these since the animation just holds */ 665 mLeftSlider.startAnimation(anim, anim); 666 mRightSlider.startAnimation(anim, anim); 667 } 668 669 public void onAnimationRepeat(Animation animation) { 670 671 } 672 673 public void onAnimationStart(Animation animation) { 674 675 } 676 677 }); 678 679 slider.hideTarget(); 680 slider.startAnimation(trans1, trans2); 681 } 682 683 private void onAnimationDone() { 684 resetView(); 685 mAnimating = false; 686 } 687 688 private boolean withinView(final float x, final float y, final View view) { 689 return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight() 690 || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth(); 691 } 692 693 private boolean isHorizontal() { 694 return mOrientation == HORIZONTAL; 695 } 696 697 private void resetView() { 698 mLeftSlider.reset(false); 699 mRightSlider.reset(false); 700 // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight()); 701 } 702 703 @Override 704 protected void onLayout(boolean changed, int l, int t, int r, int b) { 705 if (!changed) return; 706 707 // Center the widgets in the view 708 mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM); 709 mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP); 710 } 711 712 private void moveHandle(float x, float y) { 713 final View handle = mCurrentSlider.tab; 714 final View content = mCurrentSlider.text; 715 if (isHorizontal()) { 716 int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2); 717 handle.offsetLeftAndRight(deltaX); 718 content.offsetLeftAndRight(deltaX); 719 } else { 720 int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2); 721 handle.offsetTopAndBottom(deltaY); 722 content.offsetTopAndBottom(deltaY); 723 } 724 invalidate(); // TODO: be more conservative about what we're invalidating 725 } 726 727 /** 728 * Sets the left handle icon to a given resource. 729 * 730 * The resource should refer to a Drawable object, or use 0 to remove 731 * the icon. 732 * 733 * @param iconId the resource ID of the icon drawable 734 * @param targetId the resource of the target drawable 735 * @param barId the resource of the bar drawable (stateful) 736 * @param tabId the resource of the 737 */ 738 public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) { 739 mLeftSlider.setIcon(iconId); 740 mLeftSlider.setTarget(targetId); 741 mLeftSlider.setBarBackgroundResource(barId); 742 mLeftSlider.setTabBackgroundResource(tabId); 743 mLeftSlider.updateDrawableStates(); 744 } 745 746 /** 747 * Sets the left handle hint text to a given resource string. 748 * 749 * @param resId 750 */ 751 public void setLeftHintText(int resId) { 752 if (isHorizontal()) { 753 mLeftSlider.setHintText(resId); 754 } 755 } 756 757 /** 758 * Sets the right handle icon to a given resource. 759 * 760 * The resource should refer to a Drawable object, or use 0 to remove 761 * the icon. 762 * 763 * @param iconId the resource ID of the icon drawable 764 * @param targetId the resource of the target drawable 765 * @param barId the resource of the bar drawable (stateful) 766 * @param tabId the resource of the 767 */ 768 public void setRightTabResources(int iconId, int targetId, int barId, int tabId) { 769 mRightSlider.setIcon(iconId); 770 mRightSlider.setTarget(targetId); 771 mRightSlider.setBarBackgroundResource(barId); 772 mRightSlider.setTabBackgroundResource(tabId); 773 mRightSlider.updateDrawableStates(); 774 } 775 776 /** 777 * Sets the left handle hint text to a given resource string. 778 * 779 * @param resId 780 */ 781 public void setRightHintText(int resId) { 782 if (isHorizontal()) { 783 mRightSlider.setHintText(resId); 784 } 785 } 786 787 public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) { 788 mHoldLeftOnTransition = holdLeft; 789 mHoldRightOnTransition = holdRight; 790 } 791 792 /** 793 * Triggers haptic feedback. 794 */ 795 private synchronized void vibrate(long duration) { 796 if (mVibrator == null) { 797 mVibrator = (android.os.Vibrator) 798 getContext().getSystemService(Context.VIBRATOR_SERVICE); 799 } 800 mVibrator.vibrate(duration); 801 } 802 803 /** 804 * Registers a callback to be invoked when the user triggers an event. 805 * 806 * @param listener the OnDialTriggerListener to attach to this view 807 */ 808 public void setOnTriggerListener(OnTriggerListener listener) { 809 mOnTriggerListener = listener; 810 } 811 812 /** 813 * Dispatches a trigger event to listener. Ignored if a listener is not set. 814 * @param whichHandle the handle that triggered the event. 815 */ 816 private void dispatchTriggerEvent(int whichHandle) { 817 vibrate(VIBRATE_LONG); 818 if (mOnTriggerListener != null) { 819 mOnTriggerListener.onTrigger(this, whichHandle); 820 } 821 } 822 823 /** 824 * Sets the current grabbed state, and dispatches a grabbed state change 825 * event to our listener. 826 */ 827 private void setGrabbedState(int newState) { 828 if (newState != mGrabbedState) { 829 mGrabbedState = newState; 830 if (mOnTriggerListener != null) { 831 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState); 832 } 833 } 834 } 835 836 private void log(String msg) { 837 Log.d(LOG_TAG, msg); 838 } 839} 840