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