轉載請註明出處:http://blog.csdn.net/llew2011/article/details/52626148
之前寫過一篇Android UI設計之<十>自定義ListView,實現QQ空間阻尼下拉刷新和漸變菜單欄效果的文章,寫完那篇文章後想趁熱打鐵再寫一篇用ScrollView來實現同樣效果的文章,可是寫了點開頭就沒有繼續寫下去了,當時想的是等用到再寫吧,於是把它扔在了草稿箱中。近來恰好有用到,趕緊就把該文章補充完整發表出來,希望能給大家一點幫助......
使用ScrollView來實現QQ空間的阻尼下拉刷新和漸變菜單欄效果的原理和Android UI設計之<十>自定義ListView,實現QQ空間阻尼下拉刷新和漸變菜單欄效果的原理是一樣的都是用到了Android 2.3版本後的overScrollBy()方法,如果你不熟悉該方法請閱讀上篇文章,在上篇文章中我對該方法做了介紹或者是小夥伴們自行google。現在我們先看一下效果吧:
閱讀到這裏希望你已經看了我寫的這篇同類文章,沒看過也不要緊,我會帶着小夥伴們一步一步的來實現我們想要的效果,首先我們看一下QQ空間的運行效果,當滾動到最頂部後,這時候如果我們手指繼續下滑,則最頂部的View出現拉伸的效果,如果我們手指離開屏幕,則剛剛拉伸的View出現了阻尼回彈效果,當我們往上滾動時菜單欄就會隨着滾動距離的增大其透明度逐漸增大直到完全不透明,反之逐漸透明。這樣的體驗感覺很棒有木有?說實話我是非常喜歡QQ的用戶體驗,平時也喜歡模仿QQ的各種特效,撤遠了(*^__^*) ……
實現QQ空間運行效果前需要考慮兩個問題:
- 如何實現菜單透明度漸變
通過觀察QQ空間的運行效果可知其菜單欄默認爲透明,隨着滾動距離變化而變化,要想實現透明度的變化就要知道ScrollView的滾動距離,所以有關透明度的問題也就轉化成了滾動距離的問題。 - 如何實現阻尼下拉和回彈效果
要想利用ScrollView實現阻尼效果就要求ScrollView首先滾動到了頂部,當ScrollView滾動到了頂部之後若繼續手動下滑就要求其第一個Child變化來模擬下拉效果,當手指離開屏幕後該Child要恢復到初始狀態。
我們先看第一個問題:要想實現透明度漸變就要先獲取到ScrollView的滾動距離,通過滾動距離來計算出相應的透明度。在上篇文章中由於ListView的複用機制導致沒法直接的獲取到滾動距離,因此當時採用了addHeaderView()的方式,但是在ScrollVie中我們可以直接調用getScrollY()方法來獲取滾動距離,因爲該方法的返回值就是當前的滾動距離,有了滾動距離我們就可以計算出透明度了,所以在ScrollView中第一個問題那就不是事(*^__^*) ……
我們先實現菜單欄透明度漸變的功能。定義自己的ScrollView,取名爲FlexibleScrollView,單詞flexible是靈活的、多樣的的意思,因爲我們的ScrollView不僅要實現菜單欄的透明度漸變還要實現阻尼效果,所以取名爲FlexibleScrollView比較恰當。FlexibleScrollView繼承ScrollView後需要實現其構造方法,代碼如下:
public class FlexibleScrollView extends ScrollView {
public FlexibleScrollView(Context context) {
super(context);
}
public FlexibleScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlexibleScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
FlexibleScrollView僅僅是繼承了ScrollView,這本質上和ScrollView沒有區別。由於在ScrollView中可以直接通過getScrollY()方法獲取到滾動距離,所以接下來就是判斷ScrollView的滾動時機,在上篇文章中我們知道ListView發生滾動時總會調用onScrollChanged()方法,因此我們重寫了onScrollChanged()方法來計算透明度,那我們在FlexibleScrollView中是否還能重寫該方法呢?答案是OK的,熟悉ScrollView的滾動原理的童靴們應該清楚,ScrollView的滾動可分爲兩部分,一部分是手指觸摸屏幕觸發的滾動,另一部分是手指離開屏幕可能發生的滾動。
我們先看手指觸摸屏幕觸發的滾動時機,其源碼如下:
@Override
public boolean onTouchEvent(MotionEvent ev) {
......
switch (action & MotionEvent.ACTION_MASK) {
......
case MotionEvent.ACTION_MOVE:
if (mIsBeingDragged) {
......
/** 在這裏調用了onScrollChanged()方法 **/
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
......
}
break;
......
}
return true;
}
根據源碼我們知道在ScrollView的onTouchEvent()方法中當發生了ACTION_MOVE事件後總會調用onScrollChanged()方法,所以重寫該方法似乎是可行的。
接着我們再看一下當手指離開屏幕後發生的情況,源碼如下:
@Override
public boolean onTouchEvent(MotionEvent ev) {
......
switch (action & MotionEvent.ACTION_MASK) {
......
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if (getChildCount() > 0) {
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
fling(-initialVelocity);
} else {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
invalidate();
}
}
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
......
}
return true;
}
手指離開屏幕時會觸發ACTION_UP事件中,在ACTION_UP事件中,首先計算在Y軸上的滾動速度,如果手指離開屏幕時在Y軸方向上的滾動速度大於最小滾動速度時就會調用fling()方法,我們看一下fling()方法,源碼如下:/**
* Fling the scroll view
*
* @param velocityY The initial velocity in the Y direction. Positive
* numbers mean that the finger/cursor is moving down the screen,
* which means we want to scroll towards the top.
*/
public void fling(int velocityY) {
if (getChildCount() > 0) {
int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();
mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height/2);
final boolean movingDown = velocityY > 0;
if (mFlingStrictSpan == null) {
mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
}
invalidate();
}
}
在fling()方法中,參數velocityY表示在Y軸的速度(這裏補充一知識點,如果嫌ScrollView滾動過快可以重寫該方法來實現),在該方法中調用了mScroller的fling()方法,mScroller爲OverScroller類型,該類很重要,在自定義ViewGroup中特別是實現滾動的時候有不可替代的作用,有對該方法不熟悉的小夥伴請自行查閱,這裏就不再詳解了。mScroller調用完fling()方法之後通過invalidate()方法刷新頁面,刷新頁面後又輾轉調用了computeScroll()方法,該方法源碼如下:
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int oldX = mScrollX;
int oldY = mScrollY;
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
0, mOverflingDistance, false);
// 在這裏調用了onScrollChanged()方法
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (canOverscroll) {
if (y < 0 && oldY >= 0) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
} else if (y > range && oldY <= range) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
}
}
awakenScrollBars();
// Keep on drawing until the animation has finished.
postInvalidate();
} else {
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
}
可以看到在computeScroll()方法中也調用了onScrollChanged()方法,因此根據以上源碼分析可知ScrollView發送滾動時一定回調onScrollChanged()方法的,所以通過重寫該方法來計算透明度是可行的。
好了,經過一系列的源碼分析我們知道ScrollView的滾動調用了onScrollChanged()方法,所以我們就重寫該方法來計算透明度,要計算透明度我們就要知道是誰的透明度要漸變,而漸變的本質就是菜單欄View的透明度發生變化,透明度的漸變是通過alpha來控制的,所以我們需要定義表示菜單欄的背景屬性mActionBarBackground,還需要定義一個最大的滾動距離常量值mMaxScrollHeight,通過滾動距離和此最大值來計算出當前所對應的透明度,所以代碼如下:
public class FlexibleScrollView extends ScrollView {
private static final int DEFAULT_SCROLL_HEIGHT = 500;
private Drawable mActionBarBackground;
private int mMaxScrollHeight = DEFAULT_SCROLL_HEIGHT;
public FlexibleScrollView(Context context) {
super(context);
}
public FlexibleScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlexibleScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if(null != mActionBarBackground) {
mActionBarBackground.setAlpha(evaluateAlpha(Math.abs(getScrollY())));
}
}
public void bindActionBar(View actionBar) {
if(null != actionBar) {
mActionBarBackground = actionBar.getBackground();
if(null == mActionBarBackground) {
mActionBarBackground = new ColorDrawable(Color.TRANSPARENT);
}
mActionBarBackground.setAlpha(0);
if(Build.VERSION.SDK_INT >= 16) {
actionBar.setBackground(mActionBarBackground);
} else {
actionBar.setBackgroundDrawable(mActionBarBackground);
}
}
}
private int evaluateAlpha(int t) {
if (t >= mMaxScrollHeight) {
return 255;
}
return (int) (255 * t /(float) mMaxScrollHeight);
}
}
在FlexibleScrollView中我們定義了mActionBarBackground,它表示菜單欄的背景;mMaxScrollHeight表示滾動的最大距離(默認值設定爲500像素),當ScrollView的滾動值超過了這個最大滾動值,就把菜單欄的透明度設置爲不透明否則通過調用evaluateAlpha()方法計算出當前所對應的透明度。
現在菜單欄的透明度功能準備就緒了,我們先測試一下看看效果,定義菜單欄佈局文件action_bar_layout.xml,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/action_bar_height"
android:background="#aabbcc"
android:clickable="true"
android:orientation="vertical"
android:paddingLeft="10dp">
<TextView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:drawableLeft="@drawable/back"
android:text="動態"
android:textColor="#b8e7fe"
android:textSize="17sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="好友動態"
android:textColor="#b8e7fe"
android:textSize="17sp" />
</FrameLayout>
菜單欄包含一個返回按鈕和一個標題,並且給菜單欄設置了固定高度和背景色,然後佈局我們的activity_main.xml文件,代碼如下:<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.llew.sx.scroll.FlexibleScrollView
android:id="@+id/flexible_scroll_vew"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadingEdge="none"
android:fadingEdgeLength="0dp"
android:fillViewport="true"
android:scrollbars="none" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<include
android:id="@+id/flexible_header_view"
layout="@layout/flexible_header_layout" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#bbaacc"
android:gravity="center"
android:text="我是第一行" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#aaccbb"
android:gravity="center"
android:text="我是第二行" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#bbccaa"
android:gravity="center"
android:text="我是第三行" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#ccaabb"
android:gravity="center"
android:text="我是第四行" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#bcabac"
android:gravity="center"
android:text="我是第五行" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#baccba"
android:gravity="center"
android:text="我是第六行" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#abaccb"
android:gravity="center"
android:text="我是第七行" />
<TextView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#bcbcaa"
android:gravity="center"
android:text="我是第八行" />
</LinearLayout>
</com.llew.sx.scroll.FlexibleScrollView>
<include
android:id="@+id/custom_action_bar"
layout="@layout/action_bar_layout" />
</FrameLayout>
activity_main.xml的佈局文件封簡單,採用FrameLayout根佈局讓菜單欄懸浮在FlexibleListView上邊,在include標籤中引入的佈局爲了省時間我就直接複用了上篇文章的佈局文件,其代碼如下:<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/header_height">
<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/ttt" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="bottom"
android:background="#33333333"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="相冊"
android:gravity="center"
android:textColor="@android:color/white" />
<View
android:layout_width="1dp"
android:layout_height="20dp"
android:background="#ffffff" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="說說"
android:gravity="center"
android:textColor="@android:color/white" />
<View
android:layout_width="1dp"
android:layout_height="20dp"
android:background="#ffffff" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="個性化"
android:gravity="center"
android:textColor="@android:color/white" />
<View
android:layout_width="1dp"
android:layout_height="20dp"
android:background="#ffffff" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="\@ 與我相關"
android:gravity="center"
android:textColor="@android:color/white" />
</LinearLayout>
</FrameLayout>
然後接着編寫我們的MainActivity代碼,如下所示:public class MainActivity extends Activity {
private FlexibleScrollView mScrollView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mScrollView = (FlexibleScrollView) findViewById(R.id.flexible_scroll_vew);
mScrollView.bindActionBar(findViewById(R.id.custom_action_bar));
}
}
在MainActivity中通過調用FlexibleScrollView的bindActionBar()方法把懸浮菜單的背景賦值給了FlexibleScrollView的mActionBarBackground。該測試代碼很簡單,我們運行一下程序,看看效果:
看到運行效果好開心呀,(*^__^*) ……透明度漸變達到了我們的預期,接下來開始實現阻尼效果,阻尼效果就是當ScrollView滾動到了頂部此時若繼續下滑,ScrollView能夠繼續往下滾動一段距離當手指離開屏幕後ScrollView要恢復原位置。假如你看過上篇文章就應該明白我們今天實現這個功能同樣是通過重寫overScrollBy()方法(若小夥伴們有對該方法不熟悉的請點擊這裏,在這篇文章中我有對該方法做了講解)。
實現阻尼效果的核心就是重寫overScrollBy()方法,在該方法中改變HeaderView的高度,若手指鬆開我們就復原HeaderView。我們知道QQ空間頂部是一張圖片,當下拉的時候該圖片有彈性拉昇效果,當手指鬆開後圖片又伸縮回去了,所以我們就直接用ImageView模擬此效果。模擬圖片阻尼可以讓ImageView的寬高爲MATCH_PARENT(HeaderView的高度改變之後ImageView的高度也可以隨之更改),這個時候還要設置ImageView的scaleType爲CENTER_CROP(不清楚ImageView的scaleType屬性可參照我之前寫的一篇博文:Android 源碼系列之<一>從源碼的角度深入理解ImageView的ScaleType屬性)。
現在開始在修改我們的FlexibleScrollView,代碼如下:
public class FlexibleScrollView extends ScrollView {
private static final float DEFAULT_LOAD_FACTOR = 3.0F;
private static final int DEFAULT_SCROLL_HEIGHT = 500;
private View mHeaderView;
private int mOriginHeight;
private int mZoomedHeight;
private Drawable mActionBarBackground;
private int mMaxScrollHeight = DEFAULT_SCROLL_HEIGHT;
public FlexibleScrollView(Context context) {
super(context);
}
public FlexibleScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlexibleScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if(null != mActionBarBackground) {
mActionBarBackground.setAlpha(evaluateAlpha(Math.abs(getScrollY())));
}
}
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
if(null != mHeaderView) {
if(isTouchEvent && deltaY < 0) {
mHeaderView.getLayoutParams().height += Math.abs(deltaY / DEFAULT_LOAD_FACTOR);
mHeaderView.requestLayout();
mZoomedHeight = mHeaderView.getHeight();
}
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
if(null != mHeaderView && 0 != mOriginHeight && 0 != mZoomedHeight) {
int action = ev.getAction();
if(MotionEvent.ACTION_UP == action || MotionEvent.ACTION_CANCEL == action) {
resetHeaderViewHeight();
}
}
return super.onTouchEvent(ev);
}
public void bindActionBar(View actionBar) {
if(null != actionBar) {
mActionBarBackground = actionBar.getBackground();
if(null == mActionBarBackground) {
mActionBarBackground = new ColorDrawable(Color.TRANSPARENT);
}
mActionBarBackground.setAlpha(0);
if(Build.VERSION.SDK_INT >= 16) {
actionBar.setBackground(mActionBarBackground);
} else {
actionBar.setBackgroundDrawable(mActionBarBackground);
}
}
}
public void setHeaderView(View headerView) {
this.mHeaderView = headerView;
updateHeaderViewHeight();
}
private void updateHeaderViewHeight() {
mOriginHeight = null == mHeaderView ? 0 : mHeaderView.getHeight();
if(0 == mOriginHeight && null != mHeaderView) {
post(new Runnable() {
@Override
public void run() {
mOriginHeight = mHeaderView.getHeight();
}
});
}
}
private int evaluateAlpha(int t) {
if (t >= mMaxScrollHeight) {
return 255;
}
return (int) (255 * t /(float) mMaxScrollHeight);
}
private void resetHeaderViewHeight() {
if(mHeaderView.getLayoutParams().height != mOriginHeight) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(mZoomedHeight, mOriginHeight);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mHeaderView.getLayoutParams().height = (Integer) animation.getAnimatedValue();
mHeaderView.requestLayout();
}
});
valueAnimator.setDuration(200);
valueAnimator.start();
}
}
}
在FlexibleScrollView中,mHeaderView表示需要進行阻尼拉伸效果的View,mOriginHeight表示該View的原始高度,該值的獲取是通過updateHeaderViewHeight()方法來獲取的,mZoomedHeight表示該View拉伸後的高度,該值是在overScrollBy()方法中賦值的,DEFAULT_LOAD_FACTOR表示增長因子,目的是讓mHeaderView緩慢增大。最後在onTouchEvent()方法中當手指離開屏幕時回調了resetHeaderViewHeight()方法復原該View的高度。
接着修改我們的MainActivity,代碼如下:
public class MainActivity extends Activity {
private FlexibleScrollView mScrollView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mScrollView = (FlexibleScrollView) findViewById(R.id.flexible_scroll_vew);
mScrollView.bindActionBar(findViewById(R.id.custom_action_bar));
mScrollView.setHeaderView(findViewById(R.id.flexible_header_view));
}
}
下面我們運行一下,看看效果:
恩,效果看上去還不錯........
好了,有關實現QQ空間的阻尼下拉刷新和漸變菜單欄就結束了,主要是利用了2.3版本之後的overScrollBy()方法(如果要兼容2.3之前版本需要童靴們自己去實現相關邏輯);其次充分的利用了ImageView的ScaleType屬性來模擬了QQ空間圖片阻尼回彈的效果。再次感謝收看(*^__^*) ……