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