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 { onRemovePosition(int position)46 public void onRemovePosition(int position); 47 } 48 49 interface OnLayoutListener { onLayout(int l, int t, int r, int b)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 NavTabScroller(Context context, AttributeSet attrs, int defStyle)72 public NavTabScroller(Context context, AttributeSet attrs, int defStyle) { 73 super(context, attrs, defStyle); 74 init(context); 75 } 76 NavTabScroller(Context context, AttributeSet attrs)77 public NavTabScroller(Context context, AttributeSet attrs) { 78 super(context, attrs); 79 init(context); 80 } 81 NavTabScroller(Context context)82 public NavTabScroller(Context context) { 83 super(context); 84 init(context); 85 } 86 init(Context ctx)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 getScrollValue()103 protected int getScrollValue() { 104 return mHorizontal ? mScrollX : mScrollY; 105 } 106 setScrollValue(int value)107 protected void setScrollValue(int value) { 108 scrollTo(mHorizontal ? value : 0, mHorizontal ? 0 : value); 109 } 110 getTabView(int pos)111 protected NavTabView getTabView(int pos) { 112 return (NavTabView) mContentView.getChildAt(pos); 113 } 114 isHorizontal()115 protected boolean isHorizontal() { 116 return mHorizontal; 117 } 118 setOrientation(int orientation)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 onMeasure(int wspec, int hspec)132 protected void onMeasure(int wspec, int hspec) { 133 super.onMeasure(wspec, hspec); 134 calcPadding(); 135 } 136 calcPadding()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 setAdapter(BaseAdapter adapter)150 public void setAdapter(BaseAdapter adapter) { 151 setAdapter(adapter, 0); 152 } 153 154 setOnRemoveListener(OnRemoveListener l)155 public void setOnRemoveListener(OnRemoveListener l) { 156 mRemoveListener = l; 157 } 158 setOnLayoutListener(OnLayoutListener l)159 public void setOnLayoutListener(OnLayoutListener l) { 160 mLayoutListener = l; 161 } 162 setAdapter(BaseAdapter adapter, int selection)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 getContentView()181 protected ViewGroup getContentView() { 182 return mContentView; 183 } 184 getRelativeChildTop(int ix)185 protected int getRelativeChildTop(int ix) { 186 return mContentView.getChildAt(ix).getTop() - mScrollY; 187 } 188 handleDataChanged()189 protected void handleDataChanged() { 190 handleDataChanged(INVALID_POSITION); 191 } 192 handleDataChanged(int newscroll)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 finishScroller()219 protected void finishScroller() { 220 mScroller.forceFinished(true); 221 } 222 223 @Override onLayout(boolean changed, int l, int t, int r, int b)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 clearTabs()237 void clearTabs() { 238 mContentView.removeAllViews(); 239 } 240 snapToSelected(int pos, boolean smooth)241 void snapToSelected(int pos, boolean smooth) { 242 if (pos < 0) return; 243 View v = mContentView.getChildAt(pos); 244 int sx = 0; 245 int sy = 0; 246 if (mHorizontal) { 247 sx = (v.getLeft() + v.getRight() - getWidth()) / 2; 248 } else { 249 sy = (v.getTop() + v.getBottom() - getHeight()) / 2; 250 } 251 if ((sx != mScrollX) || (sy != mScrollY)) { 252 if (smooth) { 253 smoothScrollTo(sx,sy); 254 } else { 255 scrollTo(sx, sy); 256 } 257 } 258 } 259 animateOut(View v)260 protected void animateOut(View v) { 261 if (v == null) return; 262 animateOut(v, -mFlingVelocity); 263 } 264 animateOut(final View v, float velocity)265 private void animateOut(final View v, float velocity) { 266 float start = mHorizontal ? v.getTranslationY() : v.getTranslationX(); 267 animateOut(v, velocity, start); 268 } 269 animateOut(final View v, float velocity, float start)270 private void animateOut(final View v, float velocity, float start) { 271 if ((v == null) || (mAnimator != null)) return; 272 final int position = mContentView.indexOfChild(v); 273 int target = 0; 274 if (velocity < 0) { 275 target = mHorizontal ? -getHeight() : -getWidth(); 276 } else { 277 target = mHorizontal ? getHeight() : getWidth(); 278 } 279 int distance = target - (mHorizontal ? v.getTop() : v.getLeft()); 280 long duration = (long) (Math.abs(distance) * 1000 / Math.abs(velocity)); 281 int scroll = 0; 282 int translate = 0; 283 int gap = mHorizontal ? v.getWidth() : v.getHeight(); 284 int centerView = getViewCenter(v); 285 int centerScreen = getScreenCenter(); 286 int newpos = INVALID_POSITION; 287 if (centerView < centerScreen - gap / 2) { 288 // top view 289 scroll = - (centerScreen - centerView - gap); 290 translate = (position > 0) ? gap : 0; 291 newpos = position; 292 } else if (centerView > centerScreen + gap / 2) { 293 // bottom view 294 scroll = - (centerScreen + gap - centerView); 295 if (position < mAdapter.getCount() - 1) { 296 translate = -gap; 297 } 298 } else { 299 // center view 300 scroll = - (centerScreen - centerView); 301 if (position < mAdapter.getCount() - 1) { 302 translate = -gap; 303 } else { 304 scroll -= gap; 305 } 306 } 307 mGapPosition = position; 308 final int pos = newpos; 309 ObjectAnimator trans = ObjectAnimator.ofFloat(v, 310 (mHorizontal ? TRANSLATION_Y : TRANSLATION_X), start, target); 311 ObjectAnimator alpha = ObjectAnimator.ofFloat(v, ALPHA, getAlpha(v,start), 312 getAlpha(v,target)); 313 AnimatorSet set1 = new AnimatorSet(); 314 set1.playTogether(trans, alpha); 315 set1.setDuration(duration); 316 mAnimator = new AnimatorSet(); 317 ObjectAnimator trans2 = null; 318 ObjectAnimator scroll1 = null; 319 if (scroll != 0) { 320 if (mHorizontal) { 321 scroll1 = ObjectAnimator.ofInt(this, "scrollX", getScrollX(), getScrollX() + scroll); 322 } else { 323 scroll1 = ObjectAnimator.ofInt(this, "scrollY", getScrollY(), getScrollY() + scroll); 324 } 325 } 326 if (translate != 0) { 327 trans2 = ObjectAnimator.ofInt(this, "gap", 0, translate); 328 } 329 final int duration2 = 200; 330 if (scroll1 != null) { 331 if (trans2 != null) { 332 AnimatorSet set2 = new AnimatorSet(); 333 set2.playTogether(scroll1, trans2); 334 set2.setDuration(duration2); 335 mAnimator.playSequentially(set1, set2); 336 } else { 337 scroll1.setDuration(duration2); 338 mAnimator.playSequentially(set1, scroll1); 339 } 340 } else { 341 if (trans2 != null) { 342 trans2.setDuration(duration2); 343 mAnimator.playSequentially(set1, trans2); 344 } 345 } 346 mAnimator.addListener(new AnimatorListenerAdapter() { 347 public void onAnimationEnd(Animator a) { 348 if (mRemoveListener != null) { 349 mRemoveListener.onRemovePosition(position); 350 mAnimator = null; 351 mGapPosition = INVALID_POSITION; 352 mGap = 0; 353 handleDataChanged(pos); 354 } 355 } 356 }); 357 mAnimator.start(); 358 } 359 setGap(int gap)360 public void setGap(int gap) { 361 if (mGapPosition != INVALID_POSITION) { 362 mGap = gap; 363 postInvalidate(); 364 } 365 } 366 getGap()367 public int getGap() { 368 return mGap; 369 } 370 adjustGap()371 void adjustGap() { 372 for (int i = 0; i < mContentView.getChildCount(); i++) { 373 final View child = mContentView.getChildAt(i); 374 adjustViewGap(child, i); 375 } 376 } 377 adjustViewGap(View view, int pos)378 private void adjustViewGap(View view, int pos) { 379 if ((mGap < 0 && pos > mGapPosition) 380 || (mGap > 0 && pos < mGapPosition)) { 381 if (mHorizontal) { 382 view.setTranslationX(mGap); 383 } else { 384 view.setTranslationY(mGap); 385 } 386 } 387 } 388 getViewCenter(View v)389 private int getViewCenter(View v) { 390 if (mHorizontal) { 391 return v.getLeft() + v.getWidth() / 2; 392 } else { 393 return v.getTop() + v.getHeight() / 2; 394 } 395 } 396 getScreenCenter()397 private int getScreenCenter() { 398 if (mHorizontal) { 399 return getScrollX() + getWidth() / 2; 400 } else { 401 return getScrollY() + getHeight() / 2; 402 } 403 } 404 405 @Override draw(Canvas canvas)406 public void draw(Canvas canvas) { 407 if (mGapPosition > INVALID_POSITION) { 408 adjustGap(); 409 } 410 super.draw(canvas); 411 } 412 413 @Override findViewAt(int x, int y)414 protected View findViewAt(int x, int y) { 415 x += mScrollX; 416 y += mScrollY; 417 final int count = mContentView.getChildCount(); 418 for (int i = count - 1; i >= 0; i--) { 419 View child = mContentView.getChildAt(i); 420 if (child.getVisibility() == View.VISIBLE) { 421 if ((x >= child.getLeft()) && (x < child.getRight()) 422 && (y >= child.getTop()) && (y < child.getBottom())) { 423 return child; 424 } 425 } 426 } 427 return null; 428 } 429 430 @Override onOrthoDrag(View v, float distance)431 protected void onOrthoDrag(View v, float distance) { 432 if ((v != null) && (mAnimator == null)) { 433 offsetView(v, distance); 434 } 435 } 436 437 @Override onOrthoDragFinished(View downView)438 protected void onOrthoDragFinished(View downView) { 439 if (mAnimator != null) return; 440 if (mIsOrthoDragged && downView != null) { 441 // offset 442 float diff = mHorizontal ? downView.getTranslationY() : downView.getTranslationX(); 443 if (Math.abs(diff) > (mHorizontal ? downView.getHeight() : downView.getWidth()) / 2) { 444 // remove it 445 animateOut(downView, Math.signum(diff) * mFlingVelocity, diff); 446 } else { 447 // snap back 448 offsetView(downView, 0); 449 } 450 } 451 } 452 453 @Override onOrthoFling(View v, float velocity)454 protected void onOrthoFling(View v, float velocity) { 455 if (v == null) return; 456 if (mAnimator == null && Math.abs(velocity) > mFlingVelocity / 2) { 457 animateOut(v, velocity); 458 } else { 459 offsetView(v, 0); 460 } 461 } 462 offsetView(View v, float distance)463 private void offsetView(View v, float distance) { 464 v.setAlpha(getAlpha(v, distance)); 465 if (mHorizontal) { 466 v.setTranslationY(distance); 467 } else { 468 v.setTranslationX(distance); 469 } 470 } 471 getAlpha(View v, float distance)472 private float getAlpha(View v, float distance) { 473 return 1 - (float) Math.abs(distance) / (mHorizontal ? v.getHeight() : v.getWidth()); 474 } 475 ease(DecelerateInterpolator inter, float value, float start, float dist, float duration)476 private float ease(DecelerateInterpolator inter, float value, float start, 477 float dist, float duration) { 478 return start + dist * inter.getInterpolation(value / duration); 479 } 480 481 @Override onPull(int delta)482 protected void onPull(int delta) { 483 boolean layer = false; 484 int count = 2; 485 if (delta == 0 && mPullValue == 0) return; 486 if (delta == 0 && mPullValue != 0) { 487 // reset 488 for (int i = 0; i < count; i++) { 489 View child = mContentView.getChildAt((mPullValue < 0) 490 ? i 491 : mContentView.getChildCount() - 1 - i); 492 if (child == null) break; 493 ObjectAnimator trans = ObjectAnimator.ofFloat(child, 494 mHorizontal ? "translationX" : "translationY", 495 mHorizontal ? getTranslationX() : getTranslationY(), 496 0); 497 ObjectAnimator rot = ObjectAnimator.ofFloat(child, 498 mHorizontal ? "rotationY" : "rotationX", 499 mHorizontal ? getRotationY() : getRotationX(), 500 0); 501 AnimatorSet set = new AnimatorSet(); 502 set.playTogether(trans, rot); 503 set.setDuration(100); 504 set.start(); 505 } 506 mPullValue = 0; 507 } else { 508 if (mPullValue == 0) { 509 layer = true; 510 } 511 mPullValue += delta; 512 } 513 final int height = mHorizontal ? getWidth() : getHeight(); 514 int oscroll = Math.abs(mPullValue); 515 int factor = (mPullValue <= 0) ? 1 : -1; 516 for (int i = 0; i < count; i++) { 517 View child = mContentView.getChildAt((mPullValue < 0) 518 ? i 519 : mContentView.getChildCount() - 1 - i); 520 if (child == null) break; 521 if (layer) { 522 } 523 float k = PULL_FACTOR[i]; 524 float rot = -factor * ease(mCubic, oscroll, 0, k * 2, height); 525 int y = factor * (int) ease(mCubic, oscroll, 0, k*20, height); 526 if (mHorizontal) { 527 child.setTranslationX(y); 528 } else { 529 child.setTranslationY(y); 530 } 531 if (mHorizontal) { 532 child.setRotationY(-rot); 533 } else { 534 child.setRotationX(rot); 535 } 536 } 537 } 538 539 static class ContentLayout extends LinearLayout { 540 541 NavTabScroller mScroller; 542 ContentLayout(Context context, NavTabScroller scroller)543 public ContentLayout(Context context, NavTabScroller scroller) { 544 super(context); 545 mScroller = scroller; 546 } 547 548 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)549 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 550 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 551 if (mScroller.getGap() != 0) { 552 View v = getChildAt(0); 553 if (v != null) { 554 if (mScroller.isHorizontal()) { 555 int total = v.getMeasuredWidth() + getMeasuredWidth(); 556 setMeasuredDimension(total, getMeasuredHeight()); 557 } else { 558 int total = v.getMeasuredHeight() + getMeasuredHeight(); 559 setMeasuredDimension(getMeasuredWidth(), total); 560 } 561 } 562 563 } 564 } 565 566 } 567 568 }