Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of 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,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.fmradio.views;
     18 
     19 import android.animation.Animator;
     20 import android.animation.Animator.AnimatorListener;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.animation.ObjectAnimator;
     23 import android.content.Context;
     24 import android.content.res.Configuration;
     25 import android.content.res.Resources;
     26 import android.content.res.TypedArray;
     27 import android.database.Cursor;
     28 import android.graphics.Canvas;
     29 import android.graphics.Color;
     30 import android.graphics.Paint;
     31 import android.graphics.Typeface;
     32 import android.hardware.display.DisplayManagerGlobal;
     33 import android.os.Handler;
     34 import android.os.Looper;
     35 import android.util.AttributeSet;
     36 import android.util.DisplayMetrics;
     37 import android.view.Display;
     38 import android.view.DisplayInfo;
     39 import android.view.LayoutInflater;
     40 import android.view.Menu;
     41 import android.view.MenuItem;
     42 import android.view.MotionEvent;
     43 import android.view.VelocityTracker;
     44 import android.view.View;
     45 import android.view.ViewConfiguration;
     46 import android.view.ViewGroup;
     47 import android.view.ViewTreeObserver.OnPreDrawListener;
     48 import android.view.animation.Interpolator;
     49 import android.widget.AdapterView;
     50 import android.widget.AdapterView.OnItemClickListener;
     51 import android.widget.BaseAdapter;
     52 import android.widget.EdgeEffect;
     53 import android.widget.FrameLayout;
     54 import android.widget.GridView;
     55 import android.widget.ImageView;
     56 import android.widget.PopupMenu;
     57 import android.widget.PopupMenu.OnMenuItemClickListener;
     58 import android.widget.ScrollView;
     59 import android.widget.Scroller;
     60 import android.widget.TextView;
     61 
     62 import com.android.fmradio.FmStation;
     63 import com.android.fmradio.FmUtils;
     64 import com.android.fmradio.R;
     65 import com.android.fmradio.FmStation.Station;
     66 
     67 /**
     68  * Modified from Contact MultiShrinkScroll Handle the touch event and change
     69  * header size and scroll
     70  */
     71 public class FmScroller extends FrameLayout {
     72     private static final String TAG = "FmScroller";
     73 
     74     /**
     75      * 1000 pixels per millisecond. Ie, 1 pixel per second.
     76      */
     77     private static final int PIXELS_PER_SECOND = 1000;
     78     private static final int ON_PLAY_ANIMATION_DELAY = 1000;
     79     private static final int PORT_COLUMN_NUM = 3;
     80     private static final int LAND_COLUMN_NUM = 5;
     81     private static final int STATE_NO_FAVORITE = 0;
     82     private static final int STATE_HAS_FAVORITE = 1;
     83 
     84     private float[] mLastEventPosition = {
     85             0, 0
     86     };
     87     private VelocityTracker mVelocityTracker;
     88     private boolean mIsBeingDragged = false;
     89     private boolean mReceivedDown = false;
     90     private boolean mFirstOnResume = true;
     91 
     92     private String mSelection = "IS_FAVORITE=?";
     93     private String[] mSelectionArgs = {
     94         "1"
     95     };
     96 
     97     private EventListener mEventListener;
     98     private PopupMenu mPopupMenu;
     99     private Handler mMainHandler;
    100     private ScrollView mScrollView;
    101     private View mScrollViewChild;
    102     private GridView mGridView;
    103     private TextView mFavoriteText;
    104     private View mHeader;
    105     private int mMaximumHeaderHeight;
    106     private int mMinimumHeaderHeight;
    107     private Adjuster mAdjuster;
    108     private int mCurrentStation;
    109     private boolean mIsFmPlaying;
    110 
    111     private FavoriteAdapter mAdapter;
    112     private final Scroller mScroller;
    113     private final EdgeEffect mEdgeGlowBottom;
    114     private final int mTouchSlop;
    115     private final int mMaximumVelocity;
    116     private final int mMinimumVelocity;
    117     private final int mActionBarSize;
    118 
    119     private final AnimatorListener mHeaderExpandAnimationListener = new AnimatorListenerAdapter() {
    120         @Override
    121         public void onAnimationEnd(Animator animation) {
    122             refreshStateHeight();
    123         }
    124     };
    125 
    126     /**
    127      * Interpolator from android.support.v4.view.ViewPager. Snappier and more
    128      * elastic feeling than the default interpolator.
    129      */
    130     private static final Interpolator INTERPOLATOR = new Interpolator() {
    131 
    132         /**
    133          * {@inheritDoc}
    134          */
    135         @Override
    136         public float getInterpolation(float t) {
    137             t -= 1.0f;
    138             return t * t * t * t * t + 1.0f;
    139         }
    140     };
    141 
    142     /**
    143      * Constructor
    144      *
    145      * @param context The context
    146      */
    147     public FmScroller(Context context) {
    148         this(context, null);
    149     }
    150 
    151     /**
    152      * Constructor
    153      *
    154      * @param context The context
    155      * @param attrs The attrs
    156      */
    157     public FmScroller(Context context, AttributeSet attrs) {
    158         this(context, attrs, 0);
    159     }
    160 
    161     /**
    162      * Constructor
    163      *
    164      * @param context The context
    165      * @param attrs The attrs
    166      * @param defStyleAttr The default attr
    167      */
    168     public FmScroller(Context context, AttributeSet attrs, int defStyleAttr) {
    169         super(context, attrs, defStyleAttr);
    170 
    171         final ViewConfiguration configuration = ViewConfiguration.get(context);
    172         setFocusable(false);
    173 
    174         // Drawing must be enabled in order to support EdgeEffect
    175         setWillNotDraw(/* willNotDraw = */false);
    176 
    177         mEdgeGlowBottom = new EdgeEffect(context);
    178         mScroller = new Scroller(context, INTERPOLATOR);
    179         mTouchSlop = configuration.getScaledTouchSlop();
    180         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
    181         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    182 
    183         final TypedArray attributeArray = context.obtainStyledAttributes(new int[] {
    184             android.R.attr.actionBarSize
    185         });
    186         mActionBarSize = attributeArray.getDimensionPixelSize(0, 0);
    187         attributeArray.recycle();
    188     }
    189 
    190     /**
    191      * This method must be called inside the Activity's OnCreate.
    192      */
    193     public void initialize() {
    194         mScrollView = (ScrollView) findViewById(R.id.content_scroller);
    195         mScrollViewChild = findViewById(R.id.favorite_container);
    196         mHeader = findViewById(R.id.main_header_parent);
    197 
    198         mMainHandler = new Handler(Looper.getMainLooper());
    199 
    200         mFavoriteText = (TextView) findViewById(R.id.favorite_text);
    201         mGridView = (GridView) findViewById(R.id.gridview);
    202         mAdapter = new FavoriteAdapter(getContext());
    203 
    204         mAdjuster = new Adjuster(getContext());
    205 
    206         mGridView.setAdapter(mAdapter);
    207         Cursor c = getData();
    208         mAdapter.swipResult(c);
    209         mGridView.setFocusable(false);
    210         mGridView.setFocusableInTouchMode(false);
    211 
    212         mGridView.setOnItemClickListener(new OnItemClickListener() {
    213 
    214             @Override
    215             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    216                 if (mEventListener != null && mAdapter != null) {
    217                     mEventListener.onPlay(mAdapter.getFrequency(position));
    218                 }
    219 
    220                 mMainHandler.removeCallbacks(null);
    221                 mMainHandler.postDelayed(new Runnable() {
    222                     @Override
    223                     public void run() {
    224                         mMaximumHeaderHeight = getMaxHeight(STATE_HAS_FAVORITE);
    225                         expandHeader();
    226                     }
    227                 }, ON_PLAY_ANIMATION_DELAY);
    228 
    229             }
    230         });
    231 
    232         // Called when first time create activity
    233         doOnPreDraw(this, /* drawNextFrame = */false, new Runnable() {
    234             @Override
    235             public void run() {
    236                 refreshStateHeight();
    237                 setHeaderHeight(getMaximumScrollableHeaderHeight());
    238                 updateHeaderTextAndButton();
    239                 refreshFavoriteLayout();
    240             }
    241         });
    242     }
    243 
    244     /**
    245      * Runs a piece of code just before the next draw, after layout and measurement
    246      *
    247      * @param view The view depend on
    248      * @param drawNextFrame Whether to draw next frame
    249      * @param runnable The executed runnable instance
    250      */
    251     private void doOnPreDraw(final View view, final boolean drawNextFrame,
    252             final Runnable runnable) {
    253         final OnPreDrawListener listener = new OnPreDrawListener() {
    254             @Override
    255             public boolean onPreDraw() {
    256                 view.getViewTreeObserver().removeOnPreDrawListener(this);
    257                 runnable.run();
    258                 return drawNextFrame;
    259             }
    260         };
    261         view.getViewTreeObserver().addOnPreDrawListener(listener);
    262     }
    263 
    264     private void refreshFavoriteLayout() {
    265         setFavoriteTextHeight(mAdapter.getCount() == 0);
    266         setGridViewHeight(computeGridViewHeight());
    267     }
    268 
    269     private void setFavoriteTextHeight(boolean show) {
    270         if (mAdapter.getCount() == 0) {
    271             mFavoriteText.setVisibility(View.GONE);
    272         } else {
    273             mFavoriteText.setVisibility(View.VISIBLE);
    274         }
    275     }
    276 
    277     private void setGridViewHeight(int height) {
    278         final ViewGroup.LayoutParams params = mGridView.getLayoutParams();
    279         params.height = height;
    280         mGridView.setLayoutParams(params);
    281     }
    282 
    283     private int computeGridViewHeight() {
    284         int itemcount = mAdapter.getCount();
    285         if (itemcount == 0) {
    286             return 0;
    287         }
    288         int curOrientation = getResources().getConfiguration().orientation;
    289         final boolean isLandscape = curOrientation == Configuration.ORIENTATION_LANDSCAPE;
    290         int columnNum = isLandscape ? LAND_COLUMN_NUM : PORT_COLUMN_NUM;
    291         int itemHeight = (int) getResources().getDimension(R.dimen.fm_gridview_item_height);
    292         int itemPadding = (int) getResources().getDimension(R.dimen.fm_gridview_item_padding);
    293         int rownum = (int) Math.ceil(itemcount / (float) columnNum);
    294         int totalHeight = rownum * itemHeight + rownum * itemPadding;
    295         if (rownum == 2) {
    296             int minGridViewHeight = getHeight() - getMinHeight(STATE_HAS_FAVORITE) - 72;
    297             totalHeight = Math.max(totalHeight, minGridViewHeight);
    298         }
    299 
    300         return totalHeight;
    301     }
    302 
    303     @Override
    304     public boolean onInterceptTouchEvent(MotionEvent event) {
    305         // The only time we want to intercept touch events is when we are being
    306         // dragged.
    307         return shouldStartDrag(event);
    308     }
    309 
    310     private boolean shouldStartDrag(MotionEvent event) {
    311         if (mIsBeingDragged) {
    312             mIsBeingDragged = false;
    313             return false;
    314         }
    315 
    316         switch (event.getAction()) {
    317         // If we are in the middle of a fling and there is a down event,
    318         // we'll steal it and
    319         // start a drag.
    320             case MotionEvent.ACTION_DOWN:
    321                 updateLastEventPosition(event);
    322                 if (!mScroller.isFinished()) {
    323                     startDrag();
    324                     return true;
    325                 } else {
    326                     mReceivedDown = true;
    327                 }
    328                 break;
    329 
    330             // Otherwise, we will start a drag if there is enough motion in the
    331             // direction we are
    332             // capable of scrolling.
    333             case MotionEvent.ACTION_MOVE:
    334                 if (motionShouldStartDrag(event)) {
    335                     updateLastEventPosition(event);
    336                     startDrag();
    337                     return true;
    338                 }
    339                 break;
    340 
    341             default:
    342                 break;
    343         }
    344 
    345         return false;
    346     }
    347 
    348     @Override
    349     public boolean onTouchEvent(MotionEvent event) {
    350         final int action = event.getAction();
    351 
    352         if (mVelocityTracker == null) {
    353             mVelocityTracker = VelocityTracker.obtain();
    354         }
    355         mVelocityTracker.addMovement(event);
    356         if (!mIsBeingDragged) {
    357             if (shouldStartDrag(event)) {
    358                 return true;
    359             }
    360 
    361             if (action == MotionEvent.ACTION_UP && mReceivedDown) {
    362                 mReceivedDown = false;
    363                 return performClick();
    364             }
    365             return true;
    366         }
    367 
    368         switch (action) {
    369             case MotionEvent.ACTION_MOVE:
    370                 final float delta = updatePositionAndComputeDelta(event);
    371                 scrollTo(0, getScroll() + (int) delta);
    372                 mReceivedDown = false;
    373 
    374                 if (mIsBeingDragged) {
    375                     final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
    376                     if (delta > distanceFromMaxScrolling) {
    377                         // The ScrollView is being pulled upwards while there is
    378                         // no more
    379                         // content offscreen, and the view port is already fully
    380                         // expanded.
    381                         mEdgeGlowBottom.onPull(delta / getHeight(), 1 - event.getX() / getWidth());
    382                     }
    383 
    384                     if (!mEdgeGlowBottom.isFinished()) {
    385                         postInvalidateOnAnimation();
    386                     }
    387 
    388                 }
    389                 break;
    390 
    391             case MotionEvent.ACTION_UP:
    392             case MotionEvent.ACTION_CANCEL:
    393                 stopDrag(action == MotionEvent.ACTION_CANCEL);
    394                 mReceivedDown = false;
    395                 break;
    396 
    397             default:
    398                 break;
    399         }
    400 
    401         return true;
    402     }
    403 
    404     /**
    405      * Expand to maximum size or starting size. Disable clicks on the
    406      * photo until the animation is complete.
    407      */
    408     private void expandHeader() {
    409         if (getHeaderHeight() != mMaximumHeaderHeight) {
    410             // Expand header
    411             final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight",
    412                     mMaximumHeaderHeight);
    413             animator.addListener(mHeaderExpandAnimationListener);
    414             animator.setDuration(300);
    415             animator.start();
    416             // Scroll nested scroll view to its top
    417             if (mScrollView.getScrollY() != 0) {
    418                 ObjectAnimator.ofInt(mScrollView, "scrollY", 0).setDuration(300).start();
    419             }
    420         }
    421     }
    422 
    423     private void collapseHeader() {
    424         if (getHeaderHeight() != mMinimumHeaderHeight) {
    425             final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight",
    426                     mMinimumHeaderHeight);
    427             animator.addListener(mHeaderExpandAnimationListener);
    428             animator.start();
    429         }
    430     }
    431 
    432     private void startDrag() {
    433         mIsBeingDragged = true;
    434         mScroller.abortAnimation();
    435     }
    436 
    437     private void stopDrag(boolean cancelled) {
    438         mIsBeingDragged = false;
    439         if (!cancelled && getChildCount() > 0) {
    440             final float velocity = getCurrentVelocity();
    441             if (velocity > mMinimumVelocity || velocity < -mMinimumVelocity) {
    442                 fling(-velocity);
    443             }
    444         }
    445 
    446         if (mVelocityTracker != null) {
    447             mVelocityTracker.recycle();
    448             mVelocityTracker = null;
    449         }
    450 
    451         mEdgeGlowBottom.onRelease();
    452     }
    453 
    454     @Override
    455     public void scrollTo(int x, int y) {
    456         final int delta = y - getScroll();
    457         if (delta > 0) {
    458             scrollUp(delta);
    459         } else {
    460             scrollDown(delta);
    461         }
    462         updateHeaderTextAndButton();
    463     }
    464 
    465     private int getToolbarHeight() {
    466         return mHeader.getLayoutParams().height;
    467     }
    468 
    469     /**
    470      * Set the height of the toolbar and update its tint accordingly.
    471      */
    472     @FmReflection
    473     public void setHeaderHeight(int height) {
    474         final ViewGroup.LayoutParams toolbarLayoutParams = mHeader.getLayoutParams();
    475         toolbarLayoutParams.height = height;
    476         mHeader.setLayoutParams(toolbarLayoutParams);
    477         updateHeaderTextAndButton();
    478     }
    479 
    480     /**
    481      * Get header height. Used in ObjectAnimator
    482      *
    483      * @return The header height
    484      */
    485     @FmReflection
    486     public int getHeaderHeight() {
    487         return mHeader.getLayoutParams().height;
    488     }
    489 
    490     /**
    491      * Set scroll. Used in ObjectAnimator
    492      */
    493     @FmReflection
    494     public void setScroll(int scroll) {
    495         scrollTo(0, scroll);
    496     }
    497 
    498     /**
    499      * Returns the total amount scrolled inside the nested ScrollView + the amount
    500      * of shrinking performed on the ToolBar. This is the value inspected by animators.
    501      */
    502     @FmReflection
    503     public int getScroll() {
    504         return getMaximumScrollableHeaderHeight() - getToolbarHeight() + mScrollView.getScrollY();
    505     }
    506 
    507     private int getMaximumScrollableHeaderHeight() {
    508         return mMaximumHeaderHeight;
    509     }
    510 
    511     /**
    512      * A variant of {@link #getScroll} that pretends the header is never
    513      * larger than than mIntermediateHeaderHeight. This function is sometimes
    514      * needed when making scrolling decisions that will not change the header
    515      * size (ie, snapping to the bottom or top). When mIsOpenContactSquare is
    516      * true, this function considers mIntermediateHeaderHeight == mMaximumHeaderHeight,
    517      * since snapping decisions will be made relative the full header size when
    518      * mIsOpenContactSquare = true. This value should never be used in conjunction
    519      * with {@link #getScroll} values.
    520      */
    521     private int getScrollIgnoreOversizedHeaderForSnapping() {
    522         return Math.max(getMaximumScrollableHeaderHeight() - getToolbarHeight(), 0)
    523                 + mScrollView.getScrollY();
    524     }
    525 
    526     /**
    527      * Return amount of scrolling needed in order for all the visible
    528      * subviews to scroll off the bottom.
    529      */
    530     private int getScrollUntilOffBottom() {
    531         return getHeight() + getScrollIgnoreOversizedHeaderForSnapping();
    532     }
    533 
    534     @Override
    535     public void computeScroll() {
    536         if (mScroller.computeScrollOffset()) {
    537             // Examine the fling results in order to activate EdgeEffect when we
    538             // fling to the end.
    539             final int oldScroll = getScroll();
    540             scrollTo(0, mScroller.getCurrY());
    541             final int delta = mScroller.getCurrY() - oldScroll;
    542             final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
    543             if (delta > distanceFromMaxScrolling && distanceFromMaxScrolling > 0) {
    544                 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
    545             }
    546 
    547             if (!awakenScrollBars()) {
    548                 // Keep on drawing until the animation has finished.
    549                 postInvalidateOnAnimation();
    550             }
    551             if (mScroller.getCurrY() >= getMaximumScrollUpwards()) {
    552                 mScroller.abortAnimation();
    553             }
    554         }
    555     }
    556 
    557     @Override
    558     public void draw(Canvas canvas) {
    559         super.draw(canvas);
    560 
    561         if (!mEdgeGlowBottom.isFinished()) {
    562             final int restoreCount = canvas.save();
    563             final int width = getWidth() - getPaddingLeft() - getPaddingRight();
    564             final int height = getHeight();
    565 
    566             // Draw the EdgeEffect on the bottom of the Window (Or a little bit
    567             // below the bottom
    568             // of the Window if we start to scroll upwards while EdgeEffect is
    569             // visible). This
    570             // does not need to consider the case where this MultiShrinkScroller
    571             // doesn't fill
    572             // the Window, since the nested ScrollView should be set to
    573             // fillViewport.
    574             canvas.translate(-width + getPaddingLeft(), height + getMaximumScrollUpwards()
    575                     - getScroll());
    576 
    577             canvas.rotate(180, width, 0);
    578             mEdgeGlowBottom.setSize(width, height);
    579             if (mEdgeGlowBottom.draw(canvas)) {
    580                 postInvalidateOnAnimation();
    581             }
    582             canvas.restoreToCount(restoreCount);
    583         }
    584     }
    585 
    586     private float getCurrentVelocity() {
    587         if (mVelocityTracker == null) {
    588             return 0;
    589         }
    590         mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumVelocity);
    591         return mVelocityTracker.getYVelocity();
    592     }
    593 
    594     private void fling(float velocity) {
    595         // For reasons I do not understand, scrolling is less janky when
    596         // maxY=Integer.MAX_VALUE
    597         // then when maxY is set to an actual value.
    598         mScroller.fling(0, getScroll(), 0, (int) velocity, 0, 0, -Integer.MAX_VALUE,
    599                 Integer.MAX_VALUE);
    600         invalidate();
    601     }
    602 
    603     private int getMaximumScrollUpwards() {
    604         return // How much the Header view can compress
    605         getMaximumScrollableHeaderHeight() - getFullyCompressedHeaderHeight()
    606         // How much the ScrollView can scroll. 0, if child is
    607         // smaller than ScrollView.
    608                 + Math.max(0, mScrollViewChild.getHeight() - getHeight()
    609                         + getFullyCompressedHeaderHeight());
    610     }
    611 
    612     private void scrollUp(int delta) {
    613         final ViewGroup.LayoutParams toolbarLayoutParams = mHeader.getLayoutParams();
    614         if (toolbarLayoutParams.height > getFullyCompressedHeaderHeight()) {
    615             final int originalValue = toolbarLayoutParams.height;
    616             toolbarLayoutParams.height -= delta;
    617             toolbarLayoutParams.height = Math.max(toolbarLayoutParams.height,
    618                     getFullyCompressedHeaderHeight());
    619             mHeader.setLayoutParams(toolbarLayoutParams);
    620             delta -= originalValue - toolbarLayoutParams.height;
    621         }
    622         mScrollView.scrollBy(0, delta);
    623     }
    624 
    625     /**
    626      * Returns the minimum size that we want to compress the header to,
    627      * given that we don't want to allow the the ScrollView to scroll
    628      * unless there is new content off of the edge of ScrollView.
    629      */
    630     private int getFullyCompressedHeaderHeight() {
    631         int height = Math.min(Math.max(mHeader.getLayoutParams().height
    632                 - getOverflowingChildViewSize(), mMinimumHeaderHeight),
    633                 getMaximumScrollableHeaderHeight());
    634         return height;
    635     }
    636 
    637     /**
    638      * Returns the amount of mScrollViewChild that doesn't fit inside its parent. Outside size
    639      */
    640     private int getOverflowingChildViewSize() {
    641         final int usedScrollViewSpace = mScrollViewChild.getHeight();
    642         return -getHeight() + usedScrollViewSpace + mHeader.getLayoutParams().height;
    643     }
    644 
    645     private void scrollDown(int delta) {
    646         if (mScrollView.getScrollY() > 0) {
    647             final int originalValue = mScrollView.getScrollY();
    648             mScrollView.scrollBy(0, delta);
    649         }
    650     }
    651 
    652     private void updateHeaderTextAndButton() {
    653         mAdjuster.handleScroll();
    654     }
    655 
    656     private void updateLastEventPosition(MotionEvent event) {
    657         mLastEventPosition[0] = event.getX();
    658         mLastEventPosition[1] = event.getY();
    659     }
    660 
    661     private boolean motionShouldStartDrag(MotionEvent event) {
    662         final float deltaX = event.getX() - mLastEventPosition[0];
    663         final float deltaY = event.getY() - mLastEventPosition[1];
    664         final boolean draggedX = (deltaX > mTouchSlop || deltaX < -mTouchSlop);
    665         final boolean draggedY = (deltaY > mTouchSlop || deltaY < -mTouchSlop);
    666         return draggedY && !draggedX;
    667     }
    668 
    669     private float updatePositionAndComputeDelta(MotionEvent event) {
    670         final int vertical = 1;
    671         final float position = mLastEventPosition[vertical];
    672         updateLastEventPosition(event);
    673         return position - mLastEventPosition[vertical];
    674     }
    675 
    676     /**
    677      * Interpolator that enforces a specific starting velocity.
    678      * This is useful to avoid a discontinuity between dragging
    679      * speed and flinging speed. Similar to a
    680      * {@link android.view.animation.AccelerateInterpolator} in
    681      * the sense that getInterpolation() is a quadratic function.
    682      */
    683     private static class AcceleratingFlingInterpolator implements Interpolator {
    684 
    685         private final float mStartingSpeedPixelsPerFrame;
    686 
    687         private final float mDurationMs;
    688 
    689         private final int mPixelsDelta;
    690 
    691         private final float mNumberFrames;
    692 
    693         public AcceleratingFlingInterpolator(int durationMs, float startingSpeedPixelsPerSecond,
    694                 int pixelsDelta) {
    695             mStartingSpeedPixelsPerFrame = startingSpeedPixelsPerSecond / getRefreshRate();
    696             mDurationMs = durationMs;
    697             mPixelsDelta = pixelsDelta;
    698             mNumberFrames = mDurationMs / getFrameIntervalMs();
    699         }
    700 
    701         @Override
    702         public float getInterpolation(float input) {
    703             final float animationIntervalNumber = mNumberFrames * input;
    704             final float linearDelta = (animationIntervalNumber * mStartingSpeedPixelsPerFrame)
    705                     / mPixelsDelta;
    706             // Add the results of a linear interpolator (with the initial speed)
    707             // with the
    708             // results of a AccelerateInterpolator.
    709             if (mStartingSpeedPixelsPerFrame > 0) {
    710                 return Math.min(input * input + linearDelta, 1);
    711             } else {
    712                 // Initial fling was in the wrong direction, make sure that the
    713                 // quadratic component
    714                 // grows faster in order to make up for this.
    715                 return Math.min(input * (input - linearDelta) + linearDelta, 1);
    716             }
    717         }
    718 
    719         private float getRefreshRate() {
    720             DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo(
    721                     Display.DEFAULT_DISPLAY);
    722             return di.refreshRate;
    723         }
    724 
    725         public long getFrameIntervalMs() {
    726             return (long) (1000 / getRefreshRate());
    727         }
    728     }
    729 
    730     private int getMaxHeight(int state) {
    731         int height = 0;
    732         switch (state) {
    733             case STATE_NO_FAVORITE:
    734                 height = getHeight();
    735                 break;
    736             case STATE_HAS_FAVORITE:
    737                 height = (int) getResources().getDimension(R.dimen.fm_main_header_big);
    738                 break;
    739             default:
    740                 break;
    741         }
    742         return height;
    743     }
    744 
    745     private int getMinHeight(int state) {
    746         int height = 0;
    747         switch (state) {
    748             case STATE_NO_FAVORITE:
    749                 height = (int) getResources().getDimension(R.dimen.fm_main_header_big);
    750                 break;
    751             case STATE_HAS_FAVORITE:
    752                 height = (int) getResources().getDimension(R.dimen.fm_main_header_small);
    753                 break;
    754             default:
    755                 break;
    756         }
    757         return height;
    758     }
    759 
    760     private void setMinHeight(int height) {
    761         mMinimumHeaderHeight = height;
    762     }
    763 
    764     class FavoriteAdapter extends BaseAdapter {
    765         private Cursor mCursor;
    766 
    767         private LayoutInflater mInflater;
    768 
    769         public FavoriteAdapter(Context context) {
    770             mInflater = LayoutInflater.from(context);
    771         }
    772 
    773         public int getFrequency(int position) {
    774             if (mCursor != null && mCursor.moveToFirst()) {
    775                 mCursor.moveToPosition(position);
    776                 return mCursor.getInt(mCursor.getColumnIndex(FmStation.Station.FREQUENCY));
    777             }
    778             return 0;
    779         }
    780 
    781         public void swipResult(Cursor cursor) {
    782             if (null != mCursor) {
    783                 mCursor.close();
    784             }
    785             mCursor = cursor;
    786             notifyDataSetChanged();
    787         }
    788 
    789         @Override
    790         public int getCount() {
    791             if (null != mCursor) {
    792                 return mCursor.getCount();
    793             }
    794             return 0;
    795         }
    796 
    797         @Override
    798         public Object getItem(int position) {
    799             return null;
    800         }
    801 
    802         @Override
    803         public long getItemId(int position) {
    804             return 0;
    805         }
    806 
    807         @Override
    808         public View getView(int position, View convertView, ViewGroup parent) {
    809             ViewHolder viewHolder = null;
    810             if (null == convertView) {
    811                 viewHolder = new ViewHolder();
    812                 convertView = mInflater.inflate(R.layout.favorite_gridview_item, null);
    813                 viewHolder.mStationFreq = (TextView) convertView.findViewById(R.id.station_freq);
    814                 viewHolder.mPlayIndicator = (FmVisualizerView) convertView
    815                         .findViewById(R.id.fm_play_indicator);
    816                 viewHolder.mStationName = (TextView) convertView.findViewById(R.id.station_name);
    817                 viewHolder.mMoreButton = (ImageView) convertView.findViewById(R.id.station_more);
    818                 viewHolder.mPopupMenuAnchor = convertView.findViewById(R.id.popupmenu_anchor);
    819                 convertView.setTag(viewHolder);
    820             } else {
    821                 viewHolder = (ViewHolder) convertView.getTag();
    822             }
    823 
    824             if (mCursor != null && mCursor.moveToPosition(position)) {
    825                 final int stationFreq = mCursor.getInt(mCursor
    826                         .getColumnIndex(FmStation.Station.FREQUENCY));
    827                 String name = mCursor.getString(mCursor
    828                         .getColumnIndex(FmStation.Station.STATION_NAME));
    829                 String rds = mCursor.getString(mCursor
    830                         .getColumnIndex(FmStation.Station.RADIO_TEXT));
    831                 final int isFavorite = mCursor.getInt(mCursor
    832                         .getColumnIndex(FmStation.Station.IS_FAVORITE));
    833 
    834                 if (null == name || "".equals(name)) {
    835                     name = mCursor.getString(mCursor
    836                             .getColumnIndex(FmStation.Station.PROGRAM_SERVICE));
    837                 }
    838                 if (null == name || "".equals(name)) {
    839                     name = "";
    840                 }
    841 
    842                 viewHolder.mStationFreq.setText(FmUtils.formatStation(stationFreq));
    843                 viewHolder.mStationName.setText(name);
    844 
    845                 if (mCurrentStation == stationFreq) {
    846                     viewHolder.mPlayIndicator.setVisibility(View.VISIBLE);
    847                     if (mIsFmPlaying) {
    848                         viewHolder.mPlayIndicator.startAnimation();
    849                     } else {
    850                         viewHolder.mPlayIndicator.stopAnimation();
    851                     }
    852                     viewHolder.mStationFreq.setTextColor(Color.parseColor("#607D8B"));
    853                     viewHolder.mStationFreq.setAlpha(1f);
    854                     viewHolder.mStationName.setMaxLines(1);
    855                 } else {
    856                     viewHolder.mPlayIndicator.setVisibility(View.GONE);
    857                     viewHolder.mPlayIndicator.stopAnimation();
    858                     viewHolder.mStationFreq.setTextColor(Color.parseColor("#000000"));
    859                     viewHolder.mStationFreq.setAlpha(0.87f);
    860                     viewHolder.mStationName.setMaxLines(2);
    861                 }
    862 
    863                 viewHolder.mMoreButton.setTag(viewHolder.mPopupMenuAnchor);
    864                 viewHolder.mMoreButton.setOnClickListener(new OnClickListener() {
    865                     @Override
    866                     public void onClick(View v) {
    867                         // Use anchor view to fix PopupMenu postion and cover more button
    868                         View anchor = v;
    869                         if (v.getTag() != null) {
    870                             anchor = (View) v.getTag();
    871                         }
    872                         showPopupMenu(anchor, stationFreq);
    873                     }
    874                 });
    875             }
    876 
    877             return convertView;
    878         }
    879     }
    880 
    881     private Cursor getData() {
    882         Cursor cursor = getContext().getContentResolver().query(Station.CONTENT_URI,
    883                 FmStation.COLUMNS, mSelection, mSelectionArgs,
    884                 FmStation.Station.FREQUENCY);
    885         return cursor;
    886     }
    887 
    888     /**
    889      * Called when FmRadioActivity.onResume(), refresh layout
    890      */
    891     public void onResume() {
    892         Cursor c = getData();
    893         mAdapter.swipResult(c);
    894         if (mFirstOnResume) {
    895             mFirstOnResume = false;
    896         } else {
    897             refreshStateHeight();
    898             updateHeaderTextAndButton();
    899             refreshFavoriteLayout();
    900 
    901             int curOrientation = getResources().getConfiguration().orientation;
    902             final boolean isLandscape = curOrientation == Configuration.ORIENTATION_LANDSCAPE;
    903             int columnNum = isLandscape ? LAND_COLUMN_NUM : PORT_COLUMN_NUM;
    904             boolean isOneRow = c.getCount() <= columnNum;
    905 
    906             boolean hasFavoriteCurrent = c.getCount() > 0;
    907             if (mHasFavoriteWhenOnPause != hasFavoriteCurrent || isOneRow) {
    908                 setHeaderHeight(getMaximumScrollableHeaderHeight());
    909             }
    910         }
    911     }
    912 
    913     private boolean mHasFavoriteWhenOnPause = false;
    914 
    915     /**
    916      * Called when FmRadioActivity.onPause()
    917      */
    918     public void onPause() {
    919         if (mAdapter != null && mAdapter.getCount() > 0) {
    920             mHasFavoriteWhenOnPause = true;
    921         } else {
    922             mHasFavoriteWhenOnPause = false;
    923         }
    924     }
    925 
    926     /**
    927      * Notify refresh adapter when data change
    928      */
    929     public void notifyAdatperChange() {
    930         Cursor c = getData();
    931         mAdapter.swipResult(c);
    932     }
    933 
    934     private void refreshStateHeight() {
    935         if (mAdapter != null && mAdapter.getCount() > 0) {
    936             mMaximumHeaderHeight = getMaxHeight(STATE_HAS_FAVORITE);
    937             mMinimumHeaderHeight = getMinHeight(STATE_HAS_FAVORITE);
    938         } else {
    939             mMaximumHeaderHeight = getMaxHeight(STATE_NO_FAVORITE);
    940             mMinimumHeaderHeight = getMinHeight(STATE_NO_FAVORITE);
    941         }
    942     }
    943 
    944     /**
    945      * Called when add a favorite
    946      */
    947     public void onAddFavorite() {
    948         Cursor c = getData();
    949         mAdapter.swipResult(c);
    950         refreshFavoriteLayout();
    951         if (c.getCount() == 1) {
    952             // Last time count is 0, so need set STATE_NO_FAVORITE then collapse header
    953             mMinimumHeaderHeight = getMinHeight(STATE_NO_FAVORITE);
    954             mMaximumHeaderHeight = getMaxHeight(STATE_NO_FAVORITE);
    955             collapseHeader();
    956         }
    957     }
    958 
    959     /**
    960      * Called when remove a favorite
    961      */
    962     public void onRemoveFavorite() {
    963         Cursor c = getData();
    964         mAdapter.swipResult(c);
    965         refreshFavoriteLayout();
    966         if (c != null && c.getCount() == 0) {
    967             // Stop the play animation
    968             mMainHandler.removeCallbacks(null);
    969 
    970             // Last time count is 1, so need set STATE_NO_FAVORITE then expand header
    971             mMinimumHeaderHeight = getMinHeight(STATE_NO_FAVORITE);
    972             mMaximumHeaderHeight = getMaxHeight(STATE_NO_FAVORITE);
    973             expandHeader();
    974         }
    975     }
    976 
    977     private void showPopupMenu(View anchor, final int frequency) {
    978         dismissPopupMenu();
    979         mPopupMenu = new PopupMenu(getContext(), anchor);
    980         Menu menu = mPopupMenu.getMenu();
    981         mPopupMenu.getMenuInflater().inflate(R.menu.gridview_item_more_menu, menu);
    982         mPopupMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
    983             @Override
    984             public boolean onMenuItemClick(MenuItem item) {
    985                 switch (item.getItemId()) {
    986                     case R.id.remove_favorite:
    987                         if (mEventListener != null) {
    988                             mEventListener.onRemoveFavorite(frequency);
    989                         }
    990                         break;
    991                     case R.id.rename:
    992                         if (mEventListener != null) {
    993                             mEventListener.onRename(frequency);
    994                         }
    995                         break;
    996                     default:
    997                         break;
    998                 }
    999                 return false;
   1000             }
   1001         });
   1002         mPopupMenu.show();
   1003     }
   1004 
   1005     private void dismissPopupMenu() {
   1006         if (mPopupMenu != null) {
   1007             mPopupMenu.dismiss();
   1008             mPopupMenu = null;
   1009         }
   1010     }
   1011 
   1012     /**
   1013      * Called when FmRadioActivity.onDestory()
   1014      */
   1015     public void closeAdapterCursor() {
   1016         mAdapter.swipResult(null);
   1017     }
   1018 
   1019     /**
   1020      * Register a listener for GridView item event
   1021      *
   1022      * @param listener The event listener
   1023      */
   1024     public void registerListener(EventListener listener) {
   1025         mEventListener = listener;
   1026     }
   1027 
   1028     /**
   1029      * Unregister a listener for GridView item event
   1030      *
   1031      * @param listener The event listener
   1032      */
   1033     public void unregisterListener(EventListener listener) {
   1034         mEventListener = null;
   1035     }
   1036 
   1037     /**
   1038      * Listen for GridView item event: remove, rename, click play
   1039      */
   1040     public interface EventListener {
   1041         /**
   1042          * Callback when click remove favorite menu
   1043          *
   1044          * @param frequency The frequency want to remove
   1045          */
   1046         void onRemoveFavorite(int frequency);
   1047 
   1048         /**
   1049          * Callback when click rename favorite menu
   1050          *
   1051          * @param frequency The frequency want to rename
   1052          */
   1053         void onRename(int frequency);
   1054 
   1055         /**
   1056          * Callback when click gridview item to play
   1057          *
   1058          * @param frequency The frequency want to play
   1059          */
   1060         void onPlay(int frequency);
   1061     }
   1062 
   1063     /**
   1064      * Refresh the play indicator in gridview when play station or play state change
   1065      *
   1066      * @param currentStation current station
   1067      * @param isFmPlaying whether fm is playing
   1068      */
   1069     public void refreshPlayIndicator(int currentStation, boolean isFmPlaying) {
   1070         mCurrentStation = currentStation;
   1071         mIsFmPlaying = isFmPlaying;
   1072         if (mAdapter != null) {
   1073             mAdapter.notifyDataSetChanged();
   1074         }
   1075     }
   1076 
   1077     /**
   1078      * Adjust view padding and text size when scroll
   1079      */
   1080     private class Adjuster {
   1081         private final DisplayMetrics mDisplayMetrics;
   1082 
   1083         private final int mFirstTargetHeight;
   1084 
   1085         private final int mSecondTargetHeight;
   1086 
   1087         private final int mActionBarHeight = mActionBarSize;
   1088 
   1089         private final int mStatusBarHeight;
   1090 
   1091         private final int mFullHeight;// display height without status bar
   1092 
   1093         private final float mDensity;
   1094 
   1095         private final Typeface mDefaultFrequencyTypeface;
   1096 
   1097         // Text view
   1098         private TextView mFrequencyText;
   1099 
   1100         private TextView mFmDescriptionText;
   1101 
   1102         private TextView mStationNameText;
   1103 
   1104         private TextView mStationRdsText;
   1105 
   1106         /*
   1107          * The five control buttons view(previous, next, increase,
   1108          * decrease, favorite) and stop button
   1109          */
   1110         private View mControlView;
   1111 
   1112         private View mPlayButtonView;
   1113 
   1114         private final Context mContext;
   1115 
   1116         private final boolean mIsLandscape;
   1117 
   1118         private FirstRangeAdjuster mFirstRangeAdjuster;
   1119 
   1120         private SecondRangeAdjuster mSecondRangeAdjusterr;
   1121 
   1122         public Adjuster(Context context) {
   1123             mContext = context;
   1124             mDisplayMetrics = mContext.getResources().getDisplayMetrics();
   1125             mDensity = mDisplayMetrics.density;
   1126             int curOrientation = getResources().getConfiguration().orientation;
   1127             mIsLandscape = curOrientation == Configuration.ORIENTATION_LANDSCAPE;
   1128             Resources res = mContext.getResources();
   1129             mFirstTargetHeight = res.getDimensionPixelSize(R.dimen.fm_main_header_big);
   1130             mSecondTargetHeight = res.getDimensionPixelSize(R.dimen.fm_main_header_small);
   1131             mStatusBarHeight = res
   1132                     .getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
   1133             mFullHeight = mDisplayMetrics.heightPixels - mStatusBarHeight;
   1134 
   1135             mFrequencyText = (TextView) findViewById(R.id.station_value);
   1136             mFmDescriptionText = (TextView) findViewById(R.id.text_fm);
   1137             mStationNameText = (TextView) findViewById(R.id.station_name);
   1138             mStationRdsText = (TextView) findViewById(R.id.station_rds);
   1139             mControlView = findViewById(R.id.rl_imgbtnpart);
   1140             mPlayButtonView = findViewById(R.id.play_button_container);
   1141 
   1142             mFirstRangeAdjuster = new FirstRangeAdjuster();
   1143             mSecondRangeAdjusterr = new SecondRangeAdjuster();
   1144             mControlView.setMinimumWidth(mIsLandscape ? mDisplayMetrics.heightPixels
   1145                     : mDisplayMetrics.widthPixels);
   1146             mDefaultFrequencyTypeface = mFrequencyText.getTypeface();
   1147         }
   1148 
   1149         public void handleScroll() {
   1150             int height = getHeaderHeight();
   1151             if (mIsLandscape || height > mFirstTargetHeight) {
   1152                 mFirstRangeAdjuster.handleScroll();
   1153             } else if (height >= mSecondTargetHeight) {
   1154                 mSecondRangeAdjusterr.handleScroll();
   1155             }
   1156         }
   1157 
   1158         private class FirstRangeAdjuster {
   1159             protected int mTargetHeight;
   1160 
   1161             // start text size and margin
   1162             protected float mFmDescriptionTextSizeStart;
   1163 
   1164             protected float mFrequencyStartTextSize;
   1165 
   1166             protected float mStationNameTextSizeStart;
   1167 
   1168             protected float mFmDescriptionMarginTopStart;
   1169 
   1170             protected float mFmDescriptionStartPaddingLeft;
   1171 
   1172             protected float mFrequencyMarginTopStart;
   1173 
   1174             protected float mStationNameMarginTopStart;
   1175 
   1176             protected float mStationRdsMarginTopStart;
   1177 
   1178             protected float mControlViewMarginTopStart;
   1179 
   1180             // target text size and margin
   1181             protected float mFmDescriptionTextSizeTarget;
   1182 
   1183             protected float mFrequencyTextSizeTarget;
   1184 
   1185             protected float mStationNameTextSizeTarget;
   1186 
   1187             protected float mFmDescriptionMarginTopTarget;
   1188 
   1189             protected float mFrequencyMarginTopTarget;
   1190 
   1191             protected float mStationNameMarginTopTarget;
   1192 
   1193             protected float mStationRdsMarginTopTarget;
   1194 
   1195             protected float mControlViewMarginTopTarget;
   1196 
   1197             protected float mPlayButtonMarginTopStart;
   1198 
   1199             protected float mPlayButtonMarginTopTarget;
   1200 
   1201             protected float mPlayButtonHeight;
   1202 
   1203             // Padding adjust rate as linear
   1204             protected float mFmDescriptionPaddingRate;
   1205 
   1206             protected float mFrequencyPaddingRate;
   1207 
   1208             protected float mStationNamePaddingRate;
   1209 
   1210             protected float mStationRdsPaddingRate;
   1211 
   1212             protected float mControlViewPaddingRate;
   1213 
   1214             // init it with display height
   1215             protected float mPlayButtonPaddingRate;
   1216 
   1217             // Text size adjust rate as linear
   1218             // adjust from first to target critical height
   1219             protected float mFmDescriptionTextSizeRate;
   1220 
   1221             protected float mFrequencyTextSizeRate;
   1222 
   1223             // adjust before first critical height
   1224             protected float mStationNameTextSizeRate;
   1225 
   1226             public FirstRangeAdjuster() {
   1227                 Resources res = mContext.getResources();
   1228                 mTargetHeight = mFirstTargetHeight;
   1229                 // init start
   1230                 mFmDescriptionTextSizeStart = res.getDimension(R.dimen.fm_description_text_size);
   1231                 mFrequencyStartTextSize = res.getDimension(R.dimen.fm_frequency_text_size_start);
   1232                 mStationNameTextSizeStart = res
   1233                         .getDimension(R.dimen.fm_station_name_text_size_start);
   1234                 // first view, margin refer to parent
   1235                 mFmDescriptionMarginTopStart = res
   1236                         .getDimension(R.dimen.fm_description_margin_top_start) + mActionBarHeight;
   1237                 mFrequencyMarginTopStart = res.getDimension(R.dimen.fm_frequency_margin_top_start);
   1238                 mStationNameMarginTopStart = res
   1239                         .getDimension(R.dimen.fm_station_name_margin_top_start);
   1240                 mStationRdsMarginTopStart = res
   1241                         .getDimension(R.dimen.fm_station_rds_margin_top_start);
   1242                 mControlViewMarginTopStart = res
   1243                         .getDimension(R.dimen.fm_control_buttons_margin_top_start);
   1244                 // init target
   1245                 mFrequencyTextSizeTarget = res
   1246                         .getDimension(R.dimen.fm_frequency_text_size_first_target);
   1247                 mFmDescriptionTextSizeTarget = mFrequencyTextSizeTarget;
   1248                 mStationNameTextSizeTarget = res
   1249                         .getDimension(R.dimen.fm_station_name_text_size_first_target);
   1250                 mFmDescriptionMarginTopTarget = res
   1251                         .getDimension(R.dimen.fm_description_margin_top_first_target);
   1252                 mFmDescriptionStartPaddingLeft = mFrequencyText.getPaddingLeft();
   1253                 // first view, margin refer to parent if not in landscape
   1254                 if (!mIsLandscape) {
   1255                     mFmDescriptionMarginTopTarget += mActionBarHeight;
   1256                 } else {
   1257                     mFrequencyMarginTopStart += mActionBarHeight + mFmDescriptionTextSizeStart;
   1258                 }
   1259                 mFrequencyMarginTopTarget = res
   1260                         .getDimension(R.dimen.fm_frequency_margin_top_first_target);
   1261                 mStationNameMarginTopTarget = res
   1262                         .getDimension(R.dimen.fm_station_name_margin_top_first_target);
   1263                 mStationRdsMarginTopTarget = res
   1264                         .getDimension(R.dimen.fm_station_rds_margin_top_first_target);
   1265                 mControlViewMarginTopTarget = res
   1266                         .getDimension(R.dimen.fm_control_buttons_margin_top_first_target);
   1267                 // init text size and margin adjust rate
   1268                 int scrollHeight = mFullHeight - mTargetHeight;
   1269                 mFmDescriptionTextSizeRate =
   1270                         (mFmDescriptionTextSizeStart - mFmDescriptionTextSizeTarget) / scrollHeight;
   1271                 mFrequencyTextSizeRate = (mFrequencyStartTextSize - mFrequencyTextSizeTarget)
   1272                         / scrollHeight;
   1273                 mStationNameTextSizeRate = (mStationNameTextSizeStart - mStationNameTextSizeTarget)
   1274                         / scrollHeight;
   1275                 mFmDescriptionPaddingRate =
   1276                         (mFmDescriptionMarginTopStart - mFmDescriptionMarginTopTarget)
   1277                         / scrollHeight;
   1278                 mFrequencyPaddingRate = (mFrequencyMarginTopStart - mFrequencyMarginTopTarget)
   1279                         / scrollHeight;
   1280                 mStationNamePaddingRate = (mStationNameMarginTopStart - mStationNameMarginTopTarget)
   1281                         / scrollHeight;
   1282                 mStationRdsPaddingRate = (mStationRdsMarginTopStart - mStationRdsMarginTopTarget)
   1283                         / scrollHeight;
   1284                 mControlViewPaddingRate = (mControlViewMarginTopStart - mControlViewMarginTopTarget)
   1285                         / scrollHeight;
   1286                 // init play button padding, it different to others, padding top refer to parent
   1287                 mPlayButtonHeight = res.getDimension(R.dimen.play_button_height);
   1288                 mPlayButtonMarginTopStart = mFullHeight - mPlayButtonHeight - 16 * mDensity;
   1289                 mPlayButtonMarginTopTarget = mFirstTargetHeight - mPlayButtonHeight / 2;
   1290                 mPlayButtonPaddingRate = (mPlayButtonMarginTopStart - mPlayButtonMarginTopTarget)
   1291                         / scrollHeight;
   1292             }
   1293 
   1294             public void handleScroll() {
   1295                 if (mIsLandscape) {
   1296                     handleScrollLandscapeMode();
   1297                     return;
   1298                 }
   1299                 int currentHeight = getHeaderHeight();
   1300                 float newMargin = 0;
   1301                 float lastHeight = 0;
   1302                 float newTextSize;
   1303                 // 1.FM description (margin)
   1304                 newMargin = getNewSize(currentHeight, mTargetHeight, mFmDescriptionMarginTopTarget,
   1305                         mFmDescriptionPaddingRate);
   1306                 lastHeight = setNewPadding(mFmDescriptionText, newMargin);
   1307                 // 2. frequency text (text size and margin)
   1308                 newTextSize = getNewSize(currentHeight, mTargetHeight, mFrequencyTextSizeTarget,
   1309                         mFrequencyTextSizeRate);
   1310                 mFrequencyText.setTextSize(newTextSize / mDensity);
   1311                 newMargin = getNewSize(currentHeight, mTargetHeight, mFrequencyMarginTopTarget,
   1312                         mFrequencyPaddingRate);
   1313                 lastHeight = setNewPadding(mFrequencyText, newMargin + lastHeight);
   1314                 // 3. station name (margin and text size)
   1315                 newMargin = getNewSize(currentHeight, mTargetHeight, mStationNameMarginTopTarget,
   1316                         mStationNamePaddingRate);
   1317                 lastHeight = setNewPadding(mStationNameText, newMargin + lastHeight);
   1318                 newTextSize = getNewSize(currentHeight, mTargetHeight, mStationNameTextSizeTarget,
   1319                         mStationNameTextSizeRate);
   1320                 mStationNameText.setTextSize(newTextSize / mDensity);
   1321                 // 4. station rds (margin)
   1322                 newMargin = getNewSize(currentHeight, mTargetHeight, mStationRdsMarginTopTarget,
   1323                         mStationRdsPaddingRate);
   1324                 lastHeight = setNewPadding(mStationRdsText, newMargin + lastHeight);
   1325                 // 5. control buttons (margin)
   1326                 newMargin = getNewSize(currentHeight, mTargetHeight, mControlViewMarginTopTarget,
   1327                         mControlViewPaddingRate);
   1328                 setNewPadding(mControlView, newMargin + lastHeight);
   1329                 // 6. stop button (padding), it different to others, padding top refer to parent
   1330                 newMargin = getNewSize(currentHeight, mTargetHeight, mPlayButtonMarginTopTarget,
   1331                         mPlayButtonPaddingRate);
   1332                 setNewPadding(mPlayButtonView, newMargin);
   1333             }
   1334 
   1335             private void handleScrollLandscapeMode() {
   1336                 int currentHeight = getHeaderHeight();
   1337                 float newMargin = 0;
   1338                 float lastHeight = 0;
   1339                 float newTextSize;
   1340                 // 1. FM description (color, alpha and margin)
   1341                 newMargin = getNewSize(currentHeight, mTargetHeight, mFmDescriptionMarginTopTarget,
   1342                         mFmDescriptionPaddingRate);
   1343                 setNewPadding(mFmDescriptionText, newMargin);
   1344 
   1345                 newTextSize = getNewSize(currentHeight, mTargetHeight, mFmDescriptionTextSizeTarget,
   1346                         mFmDescriptionTextSizeRate);
   1347                 mFmDescriptionText.setTextSize(newTextSize / mDensity);
   1348                 boolean reachTop = (mSecondTargetHeight == getHeaderHeight());
   1349                 mFmDescriptionText.setTextColor(reachTop ? Color.WHITE
   1350                         : getResources().getColor(R.color.text_fm_color));
   1351                 mFmDescriptionText.setAlpha(reachTop ? 0.87f : 1.0f);
   1352 
   1353                 // 2. frequency text (text size, padding and margin)
   1354                 newTextSize = getNewSize(currentHeight, mTargetHeight, mFrequencyTextSizeTarget,
   1355                         mFrequencyTextSizeRate);
   1356                 mFrequencyText.setTextSize(newTextSize / mDensity);
   1357                 newMargin = getNewSize(currentHeight, mTargetHeight, mFrequencyMarginTopTarget,
   1358                         mFrequencyPaddingRate);
   1359                 // Move frequency text like "103.7" from middle to action bar in landscape,
   1360                 // or opposite direction. For example:
   1361                 // *************************          *************************
   1362                 // *                       *          * FM 103.7              *
   1363                 // * FM                    *   <-->   *                       *
   1364                 // * 103.7                 *          *                       *
   1365                 // *************************          *************************
   1366                 // "FM", "103.7" and other subviews are in a RelativeLayout (id actionbar_parent)
   1367                 // in main_header.xml. The position is controlled by the padding of each subview.
   1368                 // Because "FM" and "103.7" move up, we need to change the padding top and change
   1369                 // the padding left of "103.7".
   1370                 // The padding between "FM" and "103.7" is 0.2 (e.g. paddingRate) times
   1371                 // the length of "FM" string length.
   1372                 float paddingRate = 0.2f;
   1373                 float addPadding = (((1 + paddingRate) * computeFmDescriptionWidth())
   1374                         * (mFullHeight - currentHeight)) / (mFullHeight - mTargetHeight);
   1375                 mFrequencyText.setPadding((int) (addPadding + mFmDescriptionStartPaddingLeft),
   1376                         (int) (newMargin), mFrequencyText.getPaddingRight(),
   1377                         mFrequencyText.getPaddingBottom());
   1378                 lastHeight = newMargin + lastHeight + mFrequencyText.getTextSize();
   1379                 // If frequency text move to action bar, change it to bold
   1380                 setNewTypefaceForFrequencyText();
   1381 
   1382                 // 3. station name (text size and margin)
   1383                 newTextSize = getNewSize(currentHeight, mTargetHeight, mStationNameTextSizeTarget,
   1384                         mStationNameTextSizeRate);
   1385                 mStationNameText.setTextSize(newTextSize / mDensity);
   1386                 newMargin = getNewSize(currentHeight, mTargetHeight, mStationNameMarginTopTarget,
   1387                         mStationNamePaddingRate);
   1388                 // if move to target position, need not move over the edge of actionbar
   1389                 if (lastHeight <= mActionBarHeight) {
   1390                     lastHeight = mActionBarHeight;
   1391                 }
   1392                 lastHeight = setNewPadding(mStationNameText, newMargin + lastHeight);
   1393                 /*
   1394                  * 4. station rds (margin), in landscape with favorite
   1395                  * it need parallel to station name
   1396                  */
   1397                 newMargin = getNewSize(currentHeight, mTargetHeight, mStationRdsMarginTopTarget,
   1398                         mStationRdsPaddingRate);
   1399                 int targetHeight = mFullHeight - (mFullHeight - mTargetHeight) / 2;
   1400                 if (currentHeight <= targetHeight) {
   1401                     String stationName = "" + mStationNameText.getText();
   1402                     int stationNameTextWidth = mStationNameText.getPaddingLeft();
   1403                     if (!stationName.equals("")) {
   1404                         Paint paint = mStationNameText.getPaint();
   1405                         stationNameTextWidth += (int) paint.measureText(stationName) + 8;
   1406                     }
   1407                     mStationRdsText.setPadding((int) stationNameTextWidth,
   1408                             (int) (newMargin + lastHeight), mStationRdsText.getPaddingRight(),
   1409                             mStationRdsText.getPaddingBottom());
   1410                 } else {
   1411                     mStationRdsText.setPadding((int) (16 * mDensity),
   1412                             (int) (newMargin + lastHeight), mStationRdsText.getPaddingRight(),
   1413                             mStationRdsText.getPaddingBottom());
   1414                 }
   1415                 // 5. control buttons (margin)
   1416                 newMargin = getNewSize(currentHeight, mTargetHeight, mControlViewMarginTopTarget,
   1417                         mControlViewPaddingRate);
   1418                 setNewPadding(mControlView, newMargin + lastHeight);
   1419                 // 6. stop button (padding), it different to others, padding top refer to parent
   1420                 newMargin = getNewSize(currentHeight, mTargetHeight, mPlayButtonMarginTopTarget,
   1421                         mPlayButtonPaddingRate);
   1422                 setNewPadding(mPlayButtonView, newMargin);
   1423             }
   1424 
   1425             // Compute the text "FM" width
   1426             private float computeFmDescriptionWidth() {
   1427                 Paint paint = mFmDescriptionText.getPaint();
   1428                 return (float) paint.measureText(mFmDescriptionText.getText().toString());
   1429             }
   1430         }
   1431 
   1432         private class SecondRangeAdjuster extends FirstRangeAdjuster {
   1433             public SecondRangeAdjuster() {
   1434                 Resources res = mContext.getResources();
   1435                 mTargetHeight = mSecondTargetHeight;
   1436                 // init start
   1437                 mFrequencyStartTextSize = res
   1438                         .getDimension(R.dimen.fm_frequency_text_size_first_target);
   1439                 mStationNameTextSizeStart = res
   1440                         .getDimension(R.dimen.fm_station_name_text_size_first_target);
   1441                 mFmDescriptionMarginTopStart = res
   1442                         .getDimension(R.dimen.fm_description_margin_top_first_target)
   1443                         + mActionBarHeight;// first view, margin refer to parent
   1444                 mFrequencyMarginTopStart = res
   1445                         .getDimension(R.dimen.fm_frequency_margin_top_first_target);
   1446                 mStationNameMarginTopStart = res
   1447                         .getDimension(R.dimen.fm_station_name_margin_top_first_target);
   1448                 mStationRdsMarginTopStart = res
   1449                         .getDimension(R.dimen.fm_station_rds_margin_top_first_target);
   1450                 mControlViewMarginTopStart = res
   1451                         .getDimension(R.dimen.fm_control_buttons_margin_top_first_target);
   1452                 // init target
   1453                 mFrequencyTextSizeTarget = res
   1454                         .getDimension(R.dimen.fm_frequency_text_size_second_target);
   1455                 mStationNameTextSizeTarget = res
   1456                         .getDimension(R.dimen.fm_station_name_text_size_second_target);
   1457                 mFmDescriptionMarginTopTarget = res
   1458                         .getDimension(R.dimen.fm_description_margin_top_second_target);
   1459                 mFrequencyMarginTopTarget = res
   1460                         .getDimension(R.dimen.fm_frequency_margin_top_second_target);
   1461                 mStationNameMarginTopTarget = res
   1462                         .getDimension(R.dimen.fm_station_name_margin_top_second_target);
   1463                 mStationRdsMarginTopTarget = res
   1464                         .getDimension(R.dimen.fm_station_rds_margin_top_second_target);
   1465                 mControlViewMarginTopTarget = res
   1466                         .getDimension(R.dimen.fm_control_buttons_margin_top_second_target);
   1467                 // init text size and margin adjust rate
   1468                 float scrollHeight = mFirstTargetHeight - mTargetHeight;
   1469                 mFrequencyTextSizeRate =
   1470                         (mFrequencyStartTextSize - mFrequencyTextSizeTarget)
   1471                         / scrollHeight;
   1472                 mStationNameTextSizeRate =
   1473                         (mStationNameTextSizeStart - mStationNameTextSizeTarget)
   1474                         / scrollHeight;
   1475                 mFmDescriptionPaddingRate =
   1476                         (mFmDescriptionMarginTopStart - mFmDescriptionMarginTopTarget)
   1477 
   1478                         / scrollHeight;
   1479                 mFrequencyPaddingRate = (mFrequencyMarginTopStart - mFrequencyMarginTopTarget)
   1480                         / scrollHeight;
   1481                 mStationNamePaddingRate = (mStationNameMarginTopStart - mStationNameMarginTopTarget)
   1482                         / scrollHeight;
   1483                 mStationRdsPaddingRate = (mStationRdsMarginTopStart - mStationRdsMarginTopTarget)
   1484                         / scrollHeight;
   1485                 mControlViewPaddingRate = (mControlViewMarginTopStart - mControlViewMarginTopTarget)
   1486                         / scrollHeight;
   1487                 // init play button padding, it different to others, padding top refer to parent
   1488                 mPlayButtonHeight = res.getDimension(R.dimen.play_button_height);
   1489                 mPlayButtonMarginTopStart = mFullHeight - mPlayButtonHeight - 16 * mDensity;
   1490                 mPlayButtonMarginTopTarget = mFirstTargetHeight - mPlayButtonHeight / 2;
   1491                 mPlayButtonPaddingRate = (mPlayButtonMarginTopStart - mPlayButtonMarginTopTarget)
   1492                         / scrollHeight;
   1493             }
   1494 
   1495             @Override
   1496             public void handleScroll() {
   1497                 int currentHeight = getHeaderHeight();
   1498                 float newMargin = 0;
   1499                 float lastHeight = 0;
   1500                 float newTextSize;
   1501                 // 1. FM description (alpha and margin)
   1502                 float alpha = 0f;
   1503                 int offset = (int) ((mFirstTargetHeight - currentHeight) / mDensity);// dip
   1504                 if (offset <= 0) {
   1505                     alpha = 1f;
   1506                 } else if (offset <= 16) {
   1507                     alpha = 1 - offset / 16f;
   1508                 }
   1509                 mFmDescriptionText.setAlpha(alpha);
   1510                 newMargin = getNewSize(currentHeight, mTargetHeight, mFmDescriptionMarginTopTarget,
   1511                         mFmDescriptionPaddingRate);
   1512                 lastHeight = setNewPadding(mFmDescriptionText, newMargin);
   1513                 // 2. frequency text (text size and margin)
   1514                 newTextSize = getNewSize(currentHeight, mTargetHeight, mFrequencyTextSizeTarget,
   1515                         mFrequencyTextSizeRate);
   1516                 mFrequencyText.setTextSize(newTextSize / mDensity);
   1517                 newMargin = getNewSize(currentHeight, mTargetHeight, mFrequencyMarginTopTarget,
   1518                         mFrequencyPaddingRate);
   1519                 lastHeight = setNewPadding(mFrequencyText, newMargin + lastHeight);
   1520                 // If frequency text move to action bar, change it to bold
   1521                 setNewTypefaceForFrequencyText();
   1522                 // 3. station name (text size and margin)
   1523                 newTextSize = getNewSize(currentHeight, mTargetHeight, mStationNameTextSizeTarget,
   1524                         mStationNameTextSizeRate);
   1525                 mStationNameText.setTextSize(newTextSize / mDensity);
   1526                 newMargin = getNewSize(currentHeight, mTargetHeight, mStationNameMarginTopTarget,
   1527                         mStationNamePaddingRate);
   1528                 // if move to target position, need not move over the edge of actionbar
   1529                 if (lastHeight <= mActionBarHeight) {
   1530                     lastHeight = mActionBarHeight;
   1531                 }
   1532                 lastHeight = setNewPadding(mStationNameText, newMargin + lastHeight);
   1533                 // 4. station rds (margin)
   1534                 newMargin = getNewSize(currentHeight, mTargetHeight, mStationRdsMarginTopTarget,
   1535                         mStationRdsPaddingRate);
   1536                 lastHeight = setNewPadding(mStationRdsText, newMargin + lastHeight);
   1537                 // 5. control buttons (margin)
   1538                 newMargin = getNewSize(currentHeight, mTargetHeight, mControlViewMarginTopTarget,
   1539                         mControlViewPaddingRate);
   1540                 setNewPadding(mControlView, newMargin + lastHeight);
   1541                 // 6. stop button (padding), it different to others, padding top refer to parent
   1542                 newMargin = currentHeight - mPlayButtonHeight / 2;
   1543                 setNewPadding(mPlayButtonView, newMargin);
   1544             }
   1545         }
   1546 
   1547         private void setNewTypefaceForFrequencyText() {
   1548             boolean needBold = (mSecondTargetHeight == getHeaderHeight());
   1549             mFrequencyText.setTypeface(needBold ? Typeface.SANS_SERIF : mDefaultFrequencyTypeface);
   1550         }
   1551 
   1552         private float setNewPadding(TextView current, float newMargin) {
   1553             current.setPadding(current.getPaddingLeft(), (int) (newMargin),
   1554                     current.getPaddingRight(), current.getPaddingBottom());
   1555             float nextLayoutPadding = newMargin + current.getTextSize();
   1556             return nextLayoutPadding;
   1557         }
   1558 
   1559         private void setNewPadding(View current, float newMargin) {
   1560             float newPadding = newMargin;
   1561             current.setPadding(current.getPaddingLeft(), (int) (newPadding),
   1562                     current.getPaddingRight(), current.getPaddingBottom());
   1563         }
   1564 
   1565         private float getNewSize(int currentHeight, int targetHeight,
   1566                 float targetSize, float rate) {
   1567             if (currentHeight == targetHeight) {
   1568                 return targetSize;
   1569             }
   1570             return targetSize + (currentHeight - targetHeight) * rate;
   1571         }
   1572     }
   1573 
   1574     private final class ViewHolder {
   1575         ImageView mMoreButton;
   1576         FmVisualizerView mPlayIndicator;
   1577         TextView mStationFreq;
   1578         TextView mStationName;
   1579         View mPopupMenuAnchor;
   1580     }
   1581 }
   1582