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