SlidingTab.java revision 24ccf3c59077d81b0dd3b314822ff7dab215c165
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.internal.widget; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.content.res.TypedArray; 22import android.graphics.Rect; 23import android.os.Handler; 24import android.os.Message; 25import android.os.Vibrator; 26import android.util.AttributeSet; 27import android.util.Log; 28import android.view.Gravity; 29import android.view.MotionEvent; 30import android.view.View; 31import android.view.ViewGroup; 32import android.widget.ImageView; 33import android.widget.TextView; 34import android.widget.ImageView.ScaleType; 35import com.android.internal.R; 36 37/** 38 * A special widget containing two Sliders and a threshold for each. Moving either slider beyond 39 * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with 40 * {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE} to be called. 41 * 42 */ 43public class SlidingTab extends ViewGroup { 44 private static final String LOG_TAG = "SlidingTab"; 45 private static final boolean DBG = false; 46 private static final int HORIZONTAL = 0; // as defined in attrs.xml 47 private static final int VERTICAL = 1; 48 private static final int MSG_ANIMATE = 100; 49 50 // TODO: Make these configurable 51 private static final float TARGET_ZONE = 2.0f / 3.0f; 52 private static final long VIBRATE_SHORT = 30; 53 private static final long VIBRATE_LONG = 40; 54 55 private OnTriggerListener mOnTriggerListener; 56 private int mGrabbedState = OnTriggerListener.NO_HANDLE; 57 private boolean mTriggered = false; 58 private Vibrator mVibrator; 59 private float mDensity; // used to scale dimensions for bitmaps. 60 61 private final SlidingTabHandler mHandler = new SlidingTabHandler(); 62 63 /** 64 * Either {@link #HORIZONTAL} or {@link #VERTICAL}. 65 */ 66 private int mOrientation; 67 68 private Slider mLeftSlider; 69 private Slider mRightSlider; 70 private Slider mCurrentSlider; 71 private boolean mTracking; 72 private float mTargetZone; 73 private Slider mOtherSlider; 74 private boolean mAnimating; 75 76 /** 77 * Interface definition for a callback to be invoked when a tab is triggered 78 * by moving it beyond a target zone. 79 */ 80 public interface OnTriggerListener { 81 /** 82 * The interface was triggered because the user let go of the handle without reaching the 83 * target zone. 84 */ 85 public static final int NO_HANDLE = 0; 86 87 /** 88 * The interface was triggered because the user grabbed the left handle and moved it past 89 * the target zone. 90 */ 91 public static final int LEFT_HANDLE = 1; 92 93 /** 94 * The interface was triggered because the user grabbed the right handle and moved it past 95 * the target zone. 96 */ 97 public static final int RIGHT_HANDLE = 2; 98 99 /** 100 * Called when the user moves a handle beyond the target zone. 101 * 102 * @param v The view that was triggered. 103 * @param whichHandle Which "dial handle" the user grabbed, 104 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}. 105 */ 106 void onTrigger(View v, int whichHandle); 107 108 /** 109 * Called when the "grabbed state" changes (i.e. when the user either grabs or releases 110 * one of the handles.) 111 * 112 * @param v the view that was triggered 113 * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE}, 114 * or {@link #RIGHT_HANDLE}. 115 */ 116 void onGrabbedStateChange(View v, int grabbedState); 117 } 118 119 /** 120 * Simple container class for all things pertinent to a slider. 121 * A slider consists of 3 Views: 122 * 123 * {@link #tab} is the tab shown on the screen in the default state. 124 * {@link #text} is the view revealed as the user slides the tab out. 125 * {@link #target} is the target the user must drag the slider past to trigger the slider. 126 * 127 */ 128 private static class Slider { 129 /** 130 * Tab alignment - determines which side the tab should be drawn on 131 */ 132 public static final int ALIGN_LEFT = 0; 133 public static final int ALIGN_RIGHT = 1; 134 public static final int ALIGN_TOP = 2; 135 public static final int ALIGN_BOTTOM = 3; 136 137 /** 138 * States for the view. 139 */ 140 private static final int STATE_NORMAL = 0; 141 private static final int STATE_PRESSED = 1; 142 private static final int STATE_ACTIVE = 2; 143 144 private final ImageView tab; 145 private final TextView text; 146 private final ImageView target; 147 148 /** 149 * Constructor 150 * 151 * @param parent the container view of this one 152 * @param tabId drawable for the tab 153 * @param barId drawable for the bar 154 * @param targetId drawable for the target 155 */ 156 Slider(ViewGroup parent, int tabId, int barId, int targetId) { 157 // Create tab 158 tab = new ImageView(parent.getContext()); 159 tab.setBackgroundResource(tabId); 160 tab.setScaleType(ScaleType.CENTER); 161 tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, 162 LayoutParams.WRAP_CONTENT)); 163 164 // Create hint TextView 165 text = new TextView(parent.getContext()); 166 text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, 167 LayoutParams.FILL_PARENT)); 168 text.setBackgroundResource(barId); 169 text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal); 170 // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen 171 172 // Create target 173 target = new ImageView(parent.getContext()); 174 target.setImageResource(targetId); 175 target.setScaleType(ScaleType.CENTER); 176 target.setLayoutParams( 177 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); 178 target.setVisibility(View.INVISIBLE); 179 180 parent.addView(target); // this needs to be first - relies on painter's algorithm 181 parent.addView(tab); 182 parent.addView(text); 183 } 184 185 void setIcon(int iconId) { 186 tab.setImageResource(iconId); 187 } 188 189 void setTabBackgroundResource(int tabId) { 190 tab.setBackgroundResource(tabId); 191 } 192 193 void setBarBackgroundResource(int barId) { 194 text.setBackgroundResource(barId); 195 } 196 197 void setHintText(int resId) { 198 // TODO: Text should be blank if widget is vertical 199 text.setText(resId); 200 } 201 202 void hide() { 203 // TODO: Animate off the screen 204 text.setVisibility(View.INVISIBLE); 205 tab.setVisibility(View.INVISIBLE); 206 target.setVisibility(View.INVISIBLE); 207 } 208 209 void setState(int state) { 210 text.setPressed(state == STATE_PRESSED); 211 tab.setPressed(state == STATE_PRESSED); 212 if (state == STATE_ACTIVE) { 213 final int[] activeState = new int[] {com.android.internal.R.attr.state_active}; 214 if (text.getBackground().isStateful()) { 215 text.getBackground().setState(activeState); 216 } 217 if (tab.getBackground().isStateful()) { 218 tab.getBackground().setState(activeState); 219 } 220 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive); 221 } else { 222 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal); 223 } 224 } 225 226 void showTarget() { 227 target.setVisibility(View.VISIBLE); 228 } 229 230 void reset() { 231 setState(STATE_NORMAL); 232 text.setVisibility(View.VISIBLE); 233 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal); 234 tab.setVisibility(View.VISIBLE); 235 target.setVisibility(View.INVISIBLE); 236 } 237 238 void setTarget(int targetId) { 239 target.setImageResource(targetId); 240 } 241 242 /** 243 * Layout the given widgets within the parent. 244 * 245 * @param l the parent's left border 246 * @param t the parent's top border 247 * @param r the parent's right border 248 * @param b the parent's bottom border 249 * @param alignment which side to align the widget to 250 */ 251 void layout(int l, int t, int r, int b, int alignment) { 252 final int handleWidth = tab.getBackground().getIntrinsicWidth(); 253 final int handleHeight = tab.getBackground().getIntrinsicHeight(); 254 final int targetWidth = target.getDrawable().getIntrinsicWidth(); 255 final int targetHeight = target.getDrawable().getIntrinsicHeight(); 256 final int parentWidth = r - l; 257 final int parentHeight = b - t; 258 259 final int leftTarget = (int) (TARGET_ZONE * parentWidth) - targetWidth + handleWidth / 2; 260 final int rightTarget = (int) ((1.0f - TARGET_ZONE) * parentWidth) - handleWidth / 2; 261 final int left = (parentWidth - handleWidth) / 2; 262 final int right = left + handleWidth; 263 264 if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) { 265 // horizontal 266 final int targetTop = (parentHeight - targetHeight) / 2; 267 final int targetBottom = targetTop + targetHeight; 268 final int top = (parentHeight - handleHeight) / 2; 269 final int bottom = (parentHeight + handleHeight) / 2; 270 if (alignment == ALIGN_LEFT) { 271 tab.layout(0, top, handleWidth, bottom); 272 text.layout(0 - parentWidth, top, 0, bottom); 273 text.setGravity(Gravity.RIGHT); 274 target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom); 275 } else { 276 tab.layout(parentWidth - handleWidth, top, parentWidth, bottom); 277 text.layout(parentWidth, top, parentWidth + parentWidth, bottom); 278 target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom); 279 text.setGravity(Gravity.TOP); 280 } 281 } else { 282 // vertical 283 final int targetLeft = (parentWidth - targetWidth) / 2; 284 final int targetRight = (parentWidth + targetWidth) / 2; 285 final int top = (int) (TARGET_ZONE * parentHeight) + handleHeight / 2 - targetHeight; 286 final int bottom = (int) ((1.0f - TARGET_ZONE) * parentHeight) - handleHeight / 2; 287 if (alignment == ALIGN_TOP) { 288 tab.layout(left, 0, right, handleHeight); 289 text.layout(left, 0 - parentHeight, right, 0); 290 target.layout(targetLeft, top, targetRight, top + targetHeight); 291 } else { 292 tab.layout(left, parentHeight - handleHeight, right, parentHeight); 293 text.layout(left, parentHeight, right, parentHeight + parentHeight); 294 target.layout(targetLeft, bottom, targetRight, bottom + targetHeight); 295 } 296 } 297 } 298 299 public int getTabWidth() { 300 return tab.getDrawable().getIntrinsicWidth(); 301 } 302 303 public int getTabHeight() { 304 return tab.getDrawable().getIntrinsicHeight(); 305 } 306 } 307 308 public SlidingTab(Context context) { 309 this(context, null); 310 } 311 312 /** 313 * Constructor used when this widget is created from a layout file. 314 */ 315 public SlidingTab(Context context, AttributeSet attrs) { 316 super(context, attrs); 317 318 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab); 319 mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL); 320 a.recycle(); 321 322 Resources r = getResources(); 323 mDensity = r.getDisplayMetrics().density; 324 if (DBG) log("- Density: " + mDensity); 325 326 mLeftSlider = new Slider(this, 327 R.drawable.jog_tab_left_generic, 328 R.drawable.jog_tab_bar_left_generic, 329 R.drawable.jog_tab_target_gray); 330 mRightSlider = new Slider(this, 331 R.drawable.jog_tab_right_generic, 332 R.drawable.jog_tab_bar_right_generic, 333 R.drawable.jog_tab_target_gray); 334 335 // setBackgroundColor(0x80808080); 336 } 337 338 @Override 339 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 340 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 341 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 342 343 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 344 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 345 346 if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { 347 throw new RuntimeException(LOG_TAG + " cannot have UNSPECIFIED dimensions"); 348 } 349 350 final int leftTabWidth = (int) (mDensity * mLeftSlider.getTabWidth() + 0.5f); 351 final int rightTabWidth = (int) (mDensity * mRightSlider.getTabWidth() + 0.5f); 352 final int leftTabHeight = (int) (mDensity * mLeftSlider.getTabHeight() + 0.5f); 353 final int rightTabHeight = (int) (mDensity * mRightSlider.getTabHeight() + 0.5f); 354 final int width; 355 final int height; 356 if (isHorizontal()) { 357 width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth); 358 height = Math.max(leftTabHeight, rightTabHeight); 359 } else { 360 width = Math.max(leftTabWidth, rightTabHeight); 361 height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight); 362 } 363 setMeasuredDimension(width, height); 364 } 365 366 @Override 367 public boolean onInterceptTouchEvent(MotionEvent event) { 368 final int action = event.getAction(); 369 final float x = event.getX(); 370 final float y = event.getY(); 371 372 final Rect frame = new Rect(); 373 374 if (mAnimating) { 375 return false; 376 } 377 378 View leftHandle = mLeftSlider.tab; 379 leftHandle.getHitRect(frame); 380 boolean leftHit = frame.contains((int) x, (int) y); 381 382 View rightHandle = mRightSlider.tab; 383 rightHandle.getHitRect(frame); 384 boolean rightHit = frame.contains((int)x, (int) y); 385 386 if (!mTracking && !(leftHit || rightHit)) { 387 return false; 388 } 389 390 switch (action) { 391 case MotionEvent.ACTION_DOWN: { 392 mTracking = true; 393 mTriggered = false; 394 vibrate(VIBRATE_SHORT); 395 if (leftHit) { 396 mCurrentSlider = mLeftSlider; 397 mOtherSlider = mRightSlider; 398 mTargetZone = isHorizontal() ? TARGET_ZONE : 1.0f - TARGET_ZONE; 399 setGrabbedState(OnTriggerListener.LEFT_HANDLE); 400 } else { 401 mCurrentSlider = mRightSlider; 402 mOtherSlider = mLeftSlider; 403 mTargetZone = isHorizontal() ? 1.0f - TARGET_ZONE : TARGET_ZONE; 404 setGrabbedState(OnTriggerListener.RIGHT_HANDLE); 405 } 406 mCurrentSlider.setState(Slider.STATE_PRESSED); 407 mCurrentSlider.showTarget(); 408 mOtherSlider.hide(); 409 break; 410 } 411 } 412 413 return true; 414 } 415 416 @Override 417 public boolean onTouchEvent(MotionEvent event) { 418 if (mTracking) { 419 final int action = event.getAction(); 420 final float x = event.getX(); 421 final float y = event.getY(); 422 final View handle = mCurrentSlider.tab; 423 switch (action) { 424 case MotionEvent.ACTION_MOVE: 425 moveHandle(x, y); 426 float position = isHorizontal() ? x : y; 427 float target = mTargetZone * (isHorizontal() ? getWidth() : getHeight()); 428 boolean targetZoneReached; 429 if (isHorizontal()) { 430 targetZoneReached = mCurrentSlider == mLeftSlider ? 431 position > target : position < target; 432 } else { 433 targetZoneReached = mCurrentSlider == mLeftSlider ? 434 position < target : position > target; 435 } 436 if (!mTriggered && targetZoneReached) { 437 mTriggered = true; 438 mTracking = false; 439 mCurrentSlider.setState(Slider.STATE_ACTIVE); 440 dispatchTriggerEvent(mCurrentSlider == mLeftSlider ? 441 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE); 442 443 // TODO: This is a place holder for the real animation. It just holds 444 // the screen for 500ms. 445 mAnimating = true; 446 mHandler.postDelayed(new Runnable() { 447 public void run() { 448 resetView(); 449 mAnimating = false; 450 } 451 }, 500); 452 } 453 454 if (isHorizontal() && (y <= handle.getBottom() && y >= handle.getTop()) || 455 !isHorizontal() && (x >= handle.getLeft() && x <= handle.getRight()) ) { 456 break; 457 } 458 // Intentionally fall through - we're outside tracking rectangle 459 460 case MotionEvent.ACTION_UP: 461 case MotionEvent.ACTION_CANCEL: 462 mTracking = false; 463 mTriggered = false; 464 resetView(); 465 setGrabbedState(OnTriggerListener.NO_HANDLE); 466 break; 467 } 468 } 469 470 return mTracking || super.onTouchEvent(event); 471 } 472 473 private boolean isHorizontal() { 474 return mOrientation == HORIZONTAL; 475 } 476 477 private void resetView() { 478 mLeftSlider.reset(); 479 mRightSlider.reset(); 480 onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight()); 481 } 482 483 @Override 484 protected void onLayout(boolean changed, int l, int t, int r, int b) { 485 if (!changed) return; 486 487 // Center the widgets in the view 488 mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM); 489 mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP); 490 491 invalidate(); // TODO: be more conservative about what we're invalidating 492 } 493 494 private void moveHandle(float x, float y) { 495 final View handle = mCurrentSlider.tab; 496 final View content = mCurrentSlider.text; 497 if (isHorizontal()) { 498 int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2); 499 handle.offsetLeftAndRight(deltaX); 500 content.offsetLeftAndRight(deltaX); 501 } else { 502 int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2); 503 handle.offsetTopAndBottom(deltaY); 504 content.offsetTopAndBottom(deltaY); 505 } 506 invalidate(); // TODO: be more conservative about what we're invalidating 507 } 508 509 /** 510 * Sets the left handle icon to a given resource. 511 * 512 * The resource should refer to a Drawable object, or use 0 to remove 513 * the icon. 514 * 515 * @param iconId the resource ID of the icon drawable 516 * @param targetId the resource of the target drawable 517 * @param barId the resource of the bar drawable (stateful) 518 * @param tabId the resource of the 519 */ 520 public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) { 521 mLeftSlider.setIcon(iconId); 522 mLeftSlider.setTarget(targetId); 523 mLeftSlider.setBarBackgroundResource(barId); 524 mLeftSlider.setTabBackgroundResource(tabId); 525 } 526 527 /** 528 * Sets the left handle hint text to a given resource string. 529 * 530 * @param resId 531 */ 532 public void setLeftHintText(int resId) { 533 mLeftSlider.setHintText(resId); 534 } 535 536 /** 537 * Sets the right handle icon to a given resource. 538 * 539 * The resource should refer to a Drawable object, or use 0 to remove 540 * the icon. 541 * 542 * @param iconId the resource ID of the icon drawable 543 * @param targetId the resource of the target drawable 544 * @param barId the resource of the bar drawable (stateful) 545 * @param tabId the resource of the 546 */ 547 public void setRightTabResources(int iconId, int targetId, int barId, int tabId) { 548 mRightSlider.setIcon(iconId); 549 mRightSlider.setTarget(targetId); 550 mRightSlider.setBarBackgroundResource(barId); 551 mRightSlider.setTabBackgroundResource(tabId); 552 } 553 554 /** 555 * Sets the left handle hint text to a given resource string. 556 * 557 * @param resId 558 */ 559 public void setRightHintText(int resId) { 560 mRightSlider.setHintText(resId); 561 } 562 563 /** 564 * Triggers haptic feedback. 565 */ 566 private synchronized void vibrate(long duration) { 567 if (mVibrator == null) { 568 mVibrator = (android.os.Vibrator) 569 getContext().getSystemService(Context.VIBRATOR_SERVICE); 570 } 571 mVibrator.vibrate(duration); 572 } 573 574 /** 575 * Registers a callback to be invoked when the user triggers an event. 576 * 577 * @param listener the OnDialTriggerListener to attach to this view 578 */ 579 public void setOnTriggerListener(OnTriggerListener listener) { 580 mOnTriggerListener = listener; 581 } 582 583 /** 584 * Dispatches a trigger event to listener. Ignored if a listener is not set. 585 * @param whichHandle the handle that triggered the event. 586 */ 587 private void dispatchTriggerEvent(int whichHandle) { 588 vibrate(VIBRATE_LONG); 589 if (mOnTriggerListener != null) { 590 mOnTriggerListener.onTrigger(this, whichHandle); 591 } 592 } 593 594 /** 595 * Sets the current grabbed state, and dispatches a grabbed state change 596 * event to our listener. 597 */ 598 private void setGrabbedState(int newState) { 599 if (newState != mGrabbedState) { 600 mGrabbedState = newState; 601 if (mOnTriggerListener != null) { 602 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState); 603 } 604 } 605 } 606 607 private class SlidingTabHandler extends Handler { 608 public void handleMessage(Message m) { 609 switch (m.what) { 610 case MSG_ANIMATE: 611 doAnimation(); 612 break; 613 } 614 } 615 } 616 617 private void doAnimation() { 618 if (mAnimating) { 619 620 } 621 } 622 623 private void log(String msg) { 624 Log.d(LOG_TAG, msg); 625 } 626} 627