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