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
17package com.android.browser;
18
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;
33import android.widget.LinearLayout;
34
35import com.android.browser.view.ScrollerView;
36
37/**
38 * custom view for displaying tabs in the nav screen
39 */
40public 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}