1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.browser; 18 19 20 import android.animation.Animator; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.content.Context; 25 import android.database.DataSetObserver; 26 import android.graphics.Canvas; 27 import android.util.AttributeSet; 28 import android.view.Gravity; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.animation.DecelerateInterpolator; 32 import android.widget.BaseAdapter; 33 import android.widget.LinearLayout; 34 35 import com.android.browser.view.ScrollerView; 36 37 /** 38 * custom view for displaying tabs in the nav screen 39 */ 40 public class NavTabScroller extends ScrollerView { 41 42 static final int INVALID_POSITION = -1; 43 static final float[] PULL_FACTOR = { 2.5f, 0.9f }; 44 45 interface OnRemoveListener { 46 public void onRemovePosition(int position); 47 } 48 49 interface OnLayoutListener { 50 public void onLayout(int l, int t, int r, int b); 51 } 52 53 private ContentLayout mContentView; 54 private BaseAdapter mAdapter; 55 private OnRemoveListener mRemoveListener; 56 private OnLayoutListener mLayoutListener; 57 private int mGap; 58 private int mGapPosition; 59 private ObjectAnimator mGapAnimator; 60 61 // after drag animation velocity in pixels/sec 62 private static final float MIN_VELOCITY = 1500; 63 private AnimatorSet mAnimator; 64 65 private float mFlingVelocity; 66 private boolean mNeedsScroll; 67 private int mScrollPosition; 68 69 DecelerateInterpolator mCubic; 70 int mPullValue; 71 72 public NavTabScroller(Context context, AttributeSet attrs, int defStyle) { 73 super(context, attrs, defStyle); 74 init(context); 75 } 76 77 public NavTabScroller(Context context, AttributeSet attrs) { 78 super(context, attrs); 79 init(context); 80 } 81 82 public NavTabScroller(Context context) { 83 super(context); 84 init(context); 85 } 86 87 private void init(Context ctx) { 88 mCubic = new DecelerateInterpolator(1.5f); 89 mGapPosition = INVALID_POSITION; 90 setHorizontalScrollBarEnabled(false); 91 setVerticalScrollBarEnabled(false); 92 mContentView = new ContentLayout(ctx, this); 93 mContentView.setOrientation(LinearLayout.HORIZONTAL); 94 addView(mContentView); 95 mContentView.setLayoutParams( 96 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); 97 // ProGuard ! 98 setGap(getGap()); 99 mFlingVelocity = getContext().getResources().getDisplayMetrics().density 100 * MIN_VELOCITY; 101 } 102 103 protected int getScrollValue() { 104 return mHorizontal ? mScrollX : mScrollY; 105 } 106 107 protected void setScrollValue(int value) { 108 scrollTo(mHorizontal ? value : 0, mHorizontal ? 0 : value); 109 } 110 111 protected NavTabView getTabView(int pos) { 112 return (NavTabView) mContentView.getChildAt(pos); 113 } 114 115 protected boolean isHorizontal() { 116 return mHorizontal; 117 } 118 119 public void setOrientation(int orientation) { 120 mContentView.setOrientation(orientation); 121 if (orientation == LinearLayout.HORIZONTAL) { 122 mContentView.setLayoutParams( 123 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); 124 } else { 125 mContentView.setLayoutParams( 126 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 127 } 128 super.setOrientation(orientation); 129 } 130 131 @Override 132 protected void onMeasure(int wspec, int hspec) { 133 super.onMeasure(wspec, hspec); 134 calcPadding(); 135 } 136 137 private void calcPadding() { 138 if (mAdapter.getCount() > 0) { 139 View v = mContentView.getChildAt(0); 140 if (mHorizontal) { 141 int pad = (getMeasuredWidth() - v.getMeasuredWidth()) / 2 + 2; 142 mContentView.setPadding(pad, 0, pad, 0); 143 } else { 144 int pad = (getMeasuredHeight() - v.getMeasuredHeight()) / 2 + 2; 145 mContentView.setPadding(0, pad, 0, pad); 146 } 147 } 148 } 149 150 public void setAdapter(BaseAdapter adapter) { 151 setAdapter(adapter, 0); 152 } 153 154 155 public void setOnRemoveListener(OnRemoveListener l) { 156 mRemoveListener = l; 157 } 158 159 public void setOnLayoutListener(OnLayoutListener l) { 160 mLayoutListener = l; 161 } 162 163 protected void setAdapter(BaseAdapter adapter, int selection) { 164 mAdapter = adapter; 165 mAdapter.registerDataSetObserver(new DataSetObserver() { 166 167 @Override 168 public void onChanged() { 169 super.onChanged(); 170 handleDataChanged(); 171 } 172 173 @Override 174 public void onInvalidated() { 175 super.onInvalidated(); 176 } 177 }); 178 handleDataChanged(selection); 179 } 180 181 protected ViewGroup getContentView() { 182 return mContentView; 183 } 184 185 protected int getRelativeChildTop(int ix) { 186 return mContentView.getChildAt(ix).getTop() - mScrollY; 187 } 188 189 protected void handleDataChanged() { 190 handleDataChanged(INVALID_POSITION); 191 } 192 193 void handleDataChanged(int newscroll) { 194 int scroll = getScrollValue(); 195 if (mGapAnimator != null) { 196 mGapAnimator.cancel(); 197 } 198 mContentView.removeAllViews(); 199 for (int i = 0; i < mAdapter.getCount(); i++) { 200 View v = mAdapter.getView(i, null, mContentView); 201 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 202 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 203 lp.gravity = (mHorizontal ? Gravity.CENTER_VERTICAL : Gravity.CENTER_HORIZONTAL); 204 mContentView.addView(v, lp); 205 if (mGapPosition > INVALID_POSITION){ 206 adjustViewGap(v, i); 207 } 208 } 209 if (newscroll > INVALID_POSITION) { 210 newscroll = Math.min(mAdapter.getCount() - 1, newscroll); 211 mNeedsScroll = true; 212 mScrollPosition = newscroll; 213 requestLayout(); 214 } else { 215 setScrollValue(scroll); 216 } 217 } 218 219 protected void finishScroller() { 220 mScroller.forceFinished(true); 221 } 222 223 @Override 224 protected void onLayout(boolean changed, int l, int t, int r, int b) { 225 super.onLayout(changed, l, t, r, b); 226 if (mNeedsScroll) { 227 mScroller.forceFinished(true); 228 snapToSelected(mScrollPosition, false); 229 mNeedsScroll = false; 230 } 231 if (mLayoutListener != null) { 232 mLayoutListener.onLayout(l, t, r, b); 233 mLayoutListener = null; 234 } 235 } 236 237 void clearTabs() { 238 mContentView.removeAllViews(); 239 } 240 241 void snapToSelected(int pos, boolean smooth) { 242 if (pos < 0) return; 243 View v = mContentView.getChildAt(pos); 244 if (v == null) return; 245 int sx = 0; 246 int sy = 0; 247 if (mHorizontal) { 248 sx = (v.getLeft() + v.getRight() - getWidth()) / 2; 249 } else { 250 sy = (v.getTop() + v.getBottom() - getHeight()) / 2; 251 } 252 if ((sx != mScrollX) || (sy != mScrollY)) { 253 if (smooth) { 254 smoothScrollTo(sx,sy); 255 } else { 256 scrollTo(sx, sy); 257 } 258 } 259 } 260 261 protected void animateOut(View v) { 262 if (v == null) return; 263 animateOut(v, -mFlingVelocity); 264 } 265 266 private void animateOut(final View v, float velocity) { 267 float start = mHorizontal ? v.getTranslationY() : v.getTranslationX(); 268 animateOut(v, velocity, start); 269 } 270 271 private void animateOut(final View v, float velocity, float start) { 272 if ((v == null) || (mAnimator != null)) return; 273 final int position = mContentView.indexOfChild(v); 274 int target = 0; 275 if (velocity < 0) { 276 target = mHorizontal ? -getHeight() : -getWidth(); 277 } else { 278 target = mHorizontal ? getHeight() : getWidth(); 279 } 280 int distance = target - (mHorizontal ? v.getTop() : v.getLeft()); 281 long duration = (long) (Math.abs(distance) * 1000 / Math.abs(velocity)); 282 int scroll = 0; 283 int translate = 0; 284 int gap = mHorizontal ? v.getWidth() : v.getHeight(); 285 int centerView = getViewCenter(v); 286 int centerScreen = getScreenCenter(); 287 int newpos = INVALID_POSITION; 288 if (centerView < centerScreen - gap / 2) { 289 // top view 290 scroll = - (centerScreen - centerView - gap); 291 translate = (position > 0) ? gap : 0; 292 newpos = position; 293 } else if (centerView > centerScreen + gap / 2) { 294 // bottom view 295 scroll = - (centerScreen + gap - centerView); 296 if (position < mAdapter.getCount() - 1) { 297 translate = -gap; 298 } 299 } else { 300 // center view 301 scroll = - (centerScreen - centerView); 302 if (position < mAdapter.getCount() - 1) { 303 translate = -gap; 304 } else { 305 scroll -= gap; 306 } 307 } 308 mGapPosition = position; 309 final int pos = newpos; 310 ObjectAnimator trans = ObjectAnimator.ofFloat(v, 311 (mHorizontal ? TRANSLATION_Y : TRANSLATION_X), start, target); 312 ObjectAnimator alpha = ObjectAnimator.ofFloat(v, ALPHA, getAlpha(v,start), 313 getAlpha(v,target)); 314 AnimatorSet set1 = new AnimatorSet(); 315 set1.playTogether(trans, alpha); 316 set1.setDuration(duration); 317 mAnimator = new AnimatorSet(); 318 ObjectAnimator trans2 = null; 319 ObjectAnimator scroll1 = null; 320 if (scroll != 0) { 321 if (mHorizontal) { 322 scroll1 = ObjectAnimator.ofInt(this, "scrollX", getScrollX(), getScrollX() + scroll); 323 } else { 324 scroll1 = ObjectAnimator.ofInt(this, "scrollY", getScrollY(), getScrollY() + scroll); 325 } 326 } 327 if (translate != 0) { 328 trans2 = ObjectAnimator.ofInt(this, "gap", 0, translate); 329 } 330 final int duration2 = 200; 331 if (scroll1 != null) { 332 if (trans2 != null) { 333 AnimatorSet set2 = new AnimatorSet(); 334 set2.playTogether(scroll1, trans2); 335 set2.setDuration(duration2); 336 mAnimator.playSequentially(set1, set2); 337 } else { 338 scroll1.setDuration(duration2); 339 mAnimator.playSequentially(set1, scroll1); 340 } 341 } else { 342 if (trans2 != null) { 343 trans2.setDuration(duration2); 344 mAnimator.playSequentially(set1, trans2); 345 } 346 } 347 mAnimator.addListener(new AnimatorListenerAdapter() { 348 public void onAnimationEnd(Animator a) { 349 if (mRemoveListener != null) { 350 mRemoveListener.onRemovePosition(position); 351 mAnimator = null; 352 mGapPosition = INVALID_POSITION; 353 mGap = 0; 354 handleDataChanged(pos); 355 } 356 } 357 }); 358 mAnimator.start(); 359 } 360 361 public void setGap(int gap) { 362 if (mGapPosition != INVALID_POSITION) { 363 mGap = gap; 364 postInvalidate(); 365 } 366 } 367 368 public int getGap() { 369 return mGap; 370 } 371 372 void adjustGap() { 373 for (int i = 0; i < mContentView.getChildCount(); i++) { 374 final View child = mContentView.getChildAt(i); 375 adjustViewGap(child, i); 376 } 377 } 378 379 private void adjustViewGap(View view, int pos) { 380 if ((mGap < 0 && pos > mGapPosition) 381 || (mGap > 0 && pos < mGapPosition)) { 382 if (mHorizontal) { 383 view.setTranslationX(mGap); 384 } else { 385 view.setTranslationY(mGap); 386 } 387 } 388 } 389 390 private int getViewCenter(View v) { 391 if (mHorizontal) { 392 return v.getLeft() + v.getWidth() / 2; 393 } else { 394 return v.getTop() + v.getHeight() / 2; 395 } 396 } 397 398 private int getScreenCenter() { 399 if (mHorizontal) { 400 return getScrollX() + getWidth() / 2; 401 } else { 402 return getScrollY() + getHeight() / 2; 403 } 404 } 405 406 @Override 407 public void draw(Canvas canvas) { 408 if (mGapPosition > INVALID_POSITION) { 409 adjustGap(); 410 } 411 super.draw(canvas); 412 } 413 414 @Override 415 protected View findViewAt(int x, int y) { 416 x += mScrollX; 417 y += mScrollY; 418 final int count = mContentView.getChildCount(); 419 for (int i = count - 1; i >= 0; i--) { 420 View child = mContentView.getChildAt(i); 421 if (child.getVisibility() == View.VISIBLE) { 422 if ((x >= child.getLeft()) && (x < child.getRight()) 423 && (y >= child.getTop()) && (y < child.getBottom())) { 424 return child; 425 } 426 } 427 } 428 return null; 429 } 430 431 @Override 432 protected void onOrthoDrag(View v, float distance) { 433 if ((v != null) && (mAnimator == null)) { 434 offsetView(v, distance); 435 } 436 } 437 438 @Override 439 protected void onOrthoDragFinished(View downView) { 440 if (mAnimator != null) return; 441 if (mIsOrthoDragged && downView != null) { 442 // offset 443 float diff = mHorizontal ? downView.getTranslationY() : downView.getTranslationX(); 444 if (Math.abs(diff) > (mHorizontal ? downView.getHeight() : downView.getWidth()) / 2) { 445 // remove it 446 animateOut(downView, Math.signum(diff) * mFlingVelocity, diff); 447 } else { 448 // snap back 449 offsetView(downView, 0); 450 } 451 } 452 } 453 454 @Override 455 protected void onOrthoFling(View v, float velocity) { 456 if (v == null) return; 457 if (mAnimator == null && Math.abs(velocity) > mFlingVelocity / 2) { 458 animateOut(v, velocity); 459 } else { 460 offsetView(v, 0); 461 } 462 } 463 464 private void offsetView(View v, float distance) { 465 v.setAlpha(getAlpha(v, distance)); 466 if (mHorizontal) { 467 v.setTranslationY(distance); 468 } else { 469 v.setTranslationX(distance); 470 } 471 } 472 473 private float getAlpha(View v, float distance) { 474 return 1 - (float) Math.abs(distance) / (mHorizontal ? v.getHeight() : v.getWidth()); 475 } 476 477 private float ease(DecelerateInterpolator inter, float value, float start, 478 float dist, float duration) { 479 return start + dist * inter.getInterpolation(value / duration); 480 } 481 482 @Override 483 protected void onPull(int delta) { 484 boolean layer = false; 485 int count = 2; 486 if (delta == 0 && mPullValue == 0) return; 487 if (delta == 0 && mPullValue != 0) { 488 // reset 489 for (int i = 0; i < count; i++) { 490 View child = mContentView.getChildAt((mPullValue < 0) 491 ? i 492 : mContentView.getChildCount() - 1 - i); 493 if (child == null) break; 494 ObjectAnimator trans = ObjectAnimator.ofFloat(child, 495 mHorizontal ? "translationX" : "translationY", 496 mHorizontal ? getTranslationX() : getTranslationY(), 497 0); 498 ObjectAnimator rot = ObjectAnimator.ofFloat(child, 499 mHorizontal ? "rotationY" : "rotationX", 500 mHorizontal ? getRotationY() : getRotationX(), 501 0); 502 AnimatorSet set = new AnimatorSet(); 503 set.playTogether(trans, rot); 504 set.setDuration(100); 505 set.start(); 506 } 507 mPullValue = 0; 508 } else { 509 if (mPullValue == 0) { 510 layer = true; 511 } 512 mPullValue += delta; 513 } 514 final int height = mHorizontal ? getWidth() : getHeight(); 515 int oscroll = Math.abs(mPullValue); 516 int factor = (mPullValue <= 0) ? 1 : -1; 517 for (int i = 0; i < count; i++) { 518 View child = mContentView.getChildAt((mPullValue < 0) 519 ? i 520 : mContentView.getChildCount() - 1 - i); 521 if (child == null) break; 522 if (layer) { 523 } 524 float k = PULL_FACTOR[i]; 525 float rot = -factor * ease(mCubic, oscroll, 0, k * 2, height); 526 int y = factor * (int) ease(mCubic, oscroll, 0, k*20, height); 527 if (mHorizontal) { 528 child.setTranslationX(y); 529 } else { 530 child.setTranslationY(y); 531 } 532 if (mHorizontal) { 533 child.setRotationY(-rot); 534 } else { 535 child.setRotationX(rot); 536 } 537 } 538 } 539 540 static class ContentLayout extends LinearLayout { 541 542 NavTabScroller mScroller; 543 544 public ContentLayout(Context context, NavTabScroller scroller) { 545 super(context); 546 mScroller = scroller; 547 } 548 549 @Override 550 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 551 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 552 if (mScroller.getGap() != 0) { 553 View v = getChildAt(0); 554 if (v != null) { 555 if (mScroller.isHorizontal()) { 556 int total = v.getMeasuredWidth() + getMeasuredWidth(); 557 setMeasuredDimension(total, getMeasuredHeight()); 558 } else { 559 int total = v.getMeasuredHeight() + getMeasuredHeight(); 560 setMeasuredDimension(getMeasuredWidth(), total); 561 } 562 } 563 564 } 565 } 566 567 } 568 569 }