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