FmScroller.java revision 40cbbc57444d732004bba3c8464c979f83290279
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
17package com.android.fmradio.views;
18
19import android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.ObjectAnimator;
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.database.Cursor;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Paint;
31import android.graphics.Typeface;
32import android.hardware.display.DisplayManagerGlobal;
33import android.os.Handler;
34import android.os.Looper;
35import android.util.AttributeSet;
36import android.util.DisplayMetrics;
37import android.view.Display;
38import android.view.DisplayInfo;
39import android.view.LayoutInflater;
40import android.view.Menu;
41import android.view.MenuItem;
42import android.view.MotionEvent;
43import android.view.VelocityTracker;
44import android.view.View;
45import android.view.ViewConfiguration;
46import android.view.ViewGroup;
47import android.view.ViewTreeObserver.OnPreDrawListener;
48import android.view.animation.Interpolator;
49import android.widget.AdapterView;
50import android.widget.AdapterView.OnItemClickListener;
51import android.widget.BaseAdapter;
52import android.widget.EdgeEffect;
53import android.widget.FrameLayout;
54import android.widget.GridView;
55import android.widget.ImageView;
56import android.widget.PopupMenu;
57import android.widget.PopupMenu.OnMenuItemClickListener;
58import android.widget.ScrollView;
59import android.widget.Scroller;
60import android.widget.TextView;
61
62import com.android.fmradio.FmStation;
63import com.android.fmradio.FmUtils;
64import com.android.fmradio.R;
65import com.android.fmradio.FmStation.Station;
66
67/**
68 * Modified from Contact MultiShrinkScroll Handle the touch event and change
69 * header size and scroll
70 */
71public 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