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