Home | History | Annotate | Download | only in browser
      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 }