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      */
FmScroller(Context context)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      */
FmScroller(Context context, AttributeSet attrs)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      */
FmScroller(Context context, AttributeSet attrs, int defStyleAttr)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      */
initialize()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      */
doOnPreDraw(final View view, final boolean drawNextFrame, final Runnable runnable)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 
refreshFavoriteLayout()264     private void refreshFavoriteLayout() {
265         setFavoriteTextHeight(mAdapter.getCount() == 0);
266         setGridViewHeight(computeGridViewHeight());
267     }
268 
setFavoriteTextHeight(boolean show)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 
setGridViewHeight(int height)277     private void setGridViewHeight(int height) {
278         final ViewGroup.LayoutParams params = mGridView.getLayoutParams();
279         params.height = height;
280         mGridView.setLayoutParams(params);
281     }
282 
computeGridViewHeight()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
onInterceptTouchEvent(MotionEvent event)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 
shouldStartDrag(MotionEvent event)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
onTouchEvent(MotionEvent event)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      */
expandHeader()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 
collapseHeader()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 
startDrag()432     private void startDrag() {
433         mIsBeingDragged = true;
434         mScroller.abortAnimation();
435     }
436 
stopDrag(boolean cancelled)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
scrollTo(int x, int y)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 
getToolbarHeight()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
setHeaderHeight(int height)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
getHeaderHeight()486     public int getHeaderHeight() {
487         return mHeader.getLayoutParams().height;
488     }
489 
490     /**
491      * Set scroll. Used in ObjectAnimator
492      */
493     @FmReflection
setScroll(int scroll)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
getScroll()503     public int getScroll() {
504         return getMaximumScrollableHeaderHeight() - getToolbarHeight() + mScrollView.getScrollY();
505     }
506 
getMaximumScrollableHeaderHeight()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      */
getScrollIgnoreOversizedHeaderForSnapping()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      */
getScrollUntilOffBottom()530     private int getScrollUntilOffBottom() {
531         return getHeight() + getScrollIgnoreOversizedHeaderForSnapping();
532     }
533 
534     @Override
computeScroll()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
draw(Canvas canvas)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 
getCurrentVelocity()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 
fling(float velocity)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 
getMaximumScrollUpwards()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 
scrollUp(int delta)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      */
getFullyCompressedHeaderHeight()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      */
getOverflowingChildViewSize()640     private int getOverflowingChildViewSize() {
641         final int usedScrollViewSpace = mScrollViewChild.getHeight();
642         return -getHeight() + usedScrollViewSpace + mHeader.getLayoutParams().height;
643     }
644 
scrollDown(int delta)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 
updateHeaderTextAndButton()652     private void updateHeaderTextAndButton() {
653         mAdjuster.handleScroll();
654     }
655 
updateLastEventPosition(MotionEvent event)656     private void updateLastEventPosition(MotionEvent event) {
657         mLastEventPosition[0] = event.getX();
658         mLastEventPosition[1] = event.getY();
659     }
660 
motionShouldStartDrag(MotionEvent event)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 
updatePositionAndComputeDelta(MotionEvent event)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 
AcceleratingFlingInterpolator(int durationMs, float startingSpeedPixelsPerSecond, int pixelsDelta)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
getInterpolation(float input)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 
getRefreshRate()719         private float getRefreshRate() {
720             DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo(
721                     Display.DEFAULT_DISPLAY);
722             return di.refreshRate;
723         }
724 
getFrameIntervalMs()725         public long getFrameIntervalMs() {
726             return (long) (1000 / getRefreshRate());
727         }
728     }
729 
getMaxHeight(int state)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 
getMinHeight(int state)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 
setMinHeight(int height)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 
FavoriteAdapter(Context context)769         public FavoriteAdapter(Context context) {
770             mInflater = LayoutInflater.from(context);
771         }
772 
getFrequency(int position)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 
swipResult(Cursor cursor)781         public void swipResult(Cursor cursor) {
782             if (null != mCursor) {
783                 mCursor.close();
784             }
785             mCursor = cursor;
786             notifyDataSetChanged();
787         }
788 
789         @Override
getCount()790         public int getCount() {
791             if (null != mCursor) {
792                 return mCursor.getCount();
793             }
794             return 0;
795         }
796 
797         @Override
getItem(int position)798         public Object getItem(int position) {
799             return null;
800         }
801 
802         @Override
getItemId(int position)803         public long getItemId(int position) {
804             return 0;
805         }
806 
807         @Override
getView(int position, View convertView, ViewGroup parent)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 
getData()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      */
onResume()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      */
onPause()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      */
notifyAdatperChange()929     public void notifyAdatperChange() {
930         Cursor c = getData();
931         mAdapter.swipResult(c);
932     }
933 
refreshStateHeight()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      */
onAddFavorite()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      */
onRemoveFavorite()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 
showPopupMenu(View anchor, final int frequency)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 
dismissPopupMenu()1005     private void dismissPopupMenu() {
1006         if (mPopupMenu != null) {
1007             mPopupMenu.dismiss();
1008             mPopupMenu = null;
1009         }
1010     }
1011 
1012     /**
1013      * Called when FmRadioActivity.onDestory()
1014      */
closeAdapterCursor()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      */
registerListener(EventListener listener)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      */
unregisterListener(EventListener listener)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          */
onRemoveFavorite(int frequency)1046         void onRemoveFavorite(int frequency);
1047 
1048         /**
1049          * Callback when click rename favorite menu
1050          *
1051          * @param frequency The frequency want to rename
1052          */
onRename(int frequency)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          */
onPlay(int frequency)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      */
refreshPlayIndicator(int currentStation, boolean isFmPlaying)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 
Adjuster(Context context)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 
handleScroll()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 
FirstRangeAdjuster()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 
handleScroll()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 
handleScrollLandscapeMode()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
computeFmDescriptionWidth()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 {
SecondRangeAdjuster()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
handleScroll()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 
setNewTypefaceForFrequencyText()1547         private void setNewTypefaceForFrequencyText() {
1548             boolean needBold = (mSecondTargetHeight == getHeaderHeight());
1549             mFrequencyText.setTypeface(needBold ? Typeface.SANS_SERIF : mDefaultFrequencyTypeface);
1550         }
1551 
setNewPadding(TextView current, float newMargin)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 
setNewPadding(View current, float newMargin)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 
getNewSize(int currentHeight, int targetHeight, float targetSize, float rate)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