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