Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android UI設計之(十)自定義ListView,實現QQ空間阻尼下拉刷新和漸變菜單欄效果

Android UI設計之(十)自定義ListView,實現QQ空間阻尼下拉刷新和漸變菜單欄效果

編輯:關於Android編程

近來項目有個需求,要做個和QQ空間類似的菜單欄透明度漸變和下拉刷新帶有阻尼回彈的效果。於是花點時間動手試了試,基本上達到了QQ空間的效果,截圖如下:

\

通過觀察QQ空間的運行效果,發現當往上滾動時菜單欄會隨著滾動距離的增大其透明度組件增大直到完全不透明,反之逐漸透明。當滾動到頂部後繼續下拉會出現拉升效果當松手之後出現阻尼回彈效果。於是就通過重寫ListView模仿了QQ空間的運行效果。

實現QQ空間運行效果前需要考慮兩個問題:

如何實現菜單欄透明度漸變
通過觀察QQ空間的運行效果可知其菜單欄的透明度是根據滾動距離而動態變化的,要想實現透明度的變化就需要知道的ListView的滾動距離,所以有關透明度的問題也就轉化成了滾動距離的問題。如何實現阻尼拉升和回彈效果
要想利用ListView實現阻尼效果就要求ListView首先滾動到了頂部,當ListView滾動到了頂部之後若繼續手動下滑就要求其第一個Child變化來模擬下拉效果,當手指松開後該Child要回彈到初始狀態。

我們先看第一個問題:要想實現透明度漸變就要先獲取到ListView的滾動距離,通過滾動距離來計算相應的透明度。由於ListView的復用機制就決定了不能通過第一個可見Item的getTop()方法來得到滾動值,所以我們可以通過HeaderView來獲取滾動距離,因為Header在ListView中是不參與復用的。

下面先了解一下ListView添加HeaderView後的滾動流程:

\

上圖大致畫了ListView含有HeaderView時的三個滾動狀態,狀態一可稱為初始狀態或者是恰好滾動到最頂部狀態,此時HeaderView的getTop()值為0;狀態二為ListView的滾動中狀態,此時HeaderView沒有完全滾動出ListView邊界,getTop()的返回值為負數且其絕對值范圍在0和HeaderView的高度之間;狀態三表示的是HeaderView完全滾動出了ListView邊界,若調用getTop()得到的返回值為負數且絕對值等於HeaderView的高度(此後可理解成HeaderView一直固定在ListView的頂部)。

明白了ListView的滾動原理,我們先嘗試實現漸變菜單欄的功能。首先定義自己的ListView,取名為FlexibleListView,單詞flexible是靈活的、多樣的的意思,因為我們的ListView不僅要實現菜單欄的透明度漸變還要實現阻尼效果,所以取名為FlexibleListView比較恰當。FlexibleListView繼承ListView後需要實現其構造方法,代碼如下:

public class FlexibleListView extends ListView {

    public FlexibleListView(Context context) {
        super(context);
    }

    public FlexibleListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(21)
    public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}
FlexibleListView僅僅是繼承了ListView,這本質上和ListView沒有區別。既然我們是通過給ListView添加HeaderView的方式來判斷滾動距離,那就要獲取到HeaderView對象。怎麼獲取到HeaderView對象呢?這裡有個技巧,由於給ListView添加HeaderView最終是調用ListView的addHeaderView(View v, Object data, boolean isSelected)方法,所以我們可以重寫該方法,取到添加進來的第一個HeaderView,那怎麼判斷是第一個添加進來的HeaderView呢?因為HeaderView的添加是有序的即先添加的先繪制。所以可以定義一個代表第一個HeaderView的屬性mHeaderView,當調用到addHeaderView()方法時通過判斷mHeaderView的值是否為空,如果為空就賦值否則不賦值,代碼如下:
public class FlexibleListView extends ListView {

    private View mHeaderView;
    private int mMaxScrollHeight;

    public FlexibleListView(Context context) {
        super(context);
    }

    public FlexibleListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(21)
    public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public void addHeaderView(View v, Object data, boolean isSelectable) {
        super.addHeaderView(v, data, isSelectable);
        if(null == mHeaderView) {
            mHeaderView = v;
            mMaxScrollHeight = mHeaderView.getLayoutParams().height;
        }
    }
}

FlexibleListView中定義了mHeaderView和mMaxScrollHeight屬性,在addHeaderView()方法中對mHeaderView做非空判斷來獲取到第一個HeaderView並賦值給mHeadereView,mMaxScrollHeight表示HeaderView的最大滾動距離,當HeaderView的滾動距離超過此值我們就要設置菜單欄不透明否則就更改透明度。在這裡我直接使用了HeaderView的高度來表示其允許滾動的最大距離。

現在可以獲取到ListView的第一個HeaderView,接下來就是判斷ListView的滾動了,這時候有的童靴可能會想到采用給ListView添加ScrollListener的方式,這種方式是可行的,但我們這次不采用添加Listener的方式,如果你對ListView的源碼比較熟悉的話就清楚觸發OnItemScrollListener的回調時機是在AbsListView的invokeOnItemScrollListener()方法中,該方法源碼如下:

/**
 * Notify our scroll listener (if there is one) of a change in scroll state
 */
void invokeOnItemScrollListener() {
    if (mFastScroll != null) {
        mFastScroll.onScroll(mFirstPosition, getChildCount(), mItemCount);
    }
    if (mOnScrollListener != null) {
        mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
    }
    onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
}
invokeOnItemScrollListener()方法就是觸發滾動回調的,無論我們給不給ListView設置OnItemScrollListener那該方法都會調用,細心的同學可能發現在該方法最後調用了View的onScrollChanged()方法,這時候你恍然大悟,我們可以重寫該方法呀,當ListView發生滾動了也就調用了onScrollChange()方法,多省事呀。呵呵,恭喜你,答對了,我們今天就是采用重寫onScrollChanged()方法並在該方法中通過判斷ListView的HeaderView的滾動距離來設置菜單欄的透明度的。
現在我們清楚了ListView的滾動時機,也有了HeaderView和最大滾動距離,接下來就是分析實現漸變的條件了:要實現漸變我們就要清楚是誰要漸變,在我們的APP中可能是ActionBar,也可能是ToolBar,還有可能是我們自定義的一個ViewGroup來模擬的ActionBar,所以FlexibleListView得有個代表ActionBar的mActionBar屬性並對外提供一個方法bindActionBar(),該方法就表示把需要實現漸變的ActionBar傳遞進來,代碼如下:
public class FlexibleListView extends ListView {

    private View mActionBar;
    private View mHeaderView;
    private int mMaxScrollHeight;
    private Drawable mActionBarBackground;

    public FlexibleListView(Context context) {
        super(context);
    }

    public FlexibleListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(21)
    public FlexibleListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @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(mHeaderView.getTop())));
        }
    }

    @Override
    public void addHeaderView(View v, Object data, boolean isSelectable) {
        super.addHeaderView(v, data, isSelectable);
        if(null == mHeaderView) {
            mHeaderView = v;
            mMaxScrollHeight = mHeaderView.getLayoutParams().height;
        }
    }

    private int evaluateAlpha(int t) {
        if (t >= mMaxScrollHeight) {
            return 255;
        }
        return (int) (255 * t /(float) mMaxScrollHeight);
    }

    public void bindActionBar(View actionBar) {
        if(null != actionBar) {
            mActionBar = actionBar;
            mActionBarBackground = actionBar.getBackground();
            if(null == mActionBarBackground) {
                mActionBarBackground = new ColorDrawable(Color.TRANSPARENT);
            }
            mActionBarBackground.setAlpha(0);
            if(Build.VERSION.SDK_INT >= 16) {
                mActionBar.setBackground(mActionBarBackground);
            } else {
                mActionBar.setBackgroundDrawable(mActionBarBackground);
            }
        }
    }

    public void bindActionBar(ActionBar actionBar) {
        if(null != actionBar) {
            // TODO impl with ActionBar
            // actionBar.setBackgroundDrawable();
        }
    }
}

FlexibleListView新增了mActionBar和mActionBarBackground屬性,mActionBar代表需要漸變的菜單欄,mActionBarBackground為菜單欄的背景。其次對外提供了重載方法bindActionBar(),參數為ActionBar的方法是空實現,裡邊添加了TODO提示符並給了setBackgroundDrawable()提示(注意ActionBar實現漸變需要設置WindowFeature),希望童靴們自己可以實現出來。

FlexibleListView中重寫了onScrollChanged()方法,在該方法中通過獲取mHeaderView的getTop()值然後調用evaluateAlpha()方法計算出alpha值,evaluateAlpha()的計算很簡單,當滾動值超過了最大滾動距離mMaxScrollHeight就返回255(255表示不透明,0表示透明),否則計算出當前滾動值所對應的alpha值,最後通過調用mActionBarBackground的setAlpha()來達到mActionBar的透明度變化。

現在實現菜單欄的透明度的邏輯准備就緒了,我們先測試一下看看,定義菜單欄布局,代碼如下:


<framelayout android:background="#aabbcc" android:clickable="true" android:layout_height="@dimen/action_bar_height" android:layout_width="match_parent" android:orientation="vertical" android:paddingleft="10dp" xmlns:android="http://schemas.android.com/apk/res/android">

    

    

</framelayout>
菜單欄包含一個返回按鈕和一個標題,並且給菜單欄設置了固定高度和背景色,然後布局我們的activity_main.xml文件,代碼如下:

<framelayout android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">

    

    
</framelayout>
activity_main.xml的布局文件很簡單,采用FrameLayout根布局讓菜單欄懸浮在FlexibleListView上邊,然後編寫我們的MainActivity代碼,如下所示:
public class MainActivity extends AppCompatActivity {

    private FlexibleListView mListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initGlobalParams();
    }

    private void initGlobalParams() {
        mListView = (FlexibleListView) findViewById(R.id.flexible_list_view);

        View mFlexibleHeaderView = new View(getApplicationContext());
        mFlexibleHeaderView.setBackgroundColor(Color.parseColor("#bbaacc"));
        int height = getResources().getDimensionPixelSize(R.dimen.header_height);
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, height);
        mFlexibleHeaderView.setLayoutParams(params);

        final View actionBar = findViewById(R.id.custom_action_bar);

        mListView.bindActionBar(actionBar);
        mListView.addHeaderView(mFlexibleHeaderView);

        mListView.setAdapter(new Adapter());
    }

    static class Adapter extends BaseAdapter {
        @Override
        public int getCount() {
            return 80;
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            TextView textView = new TextView(parent.getContext());
            textView.setPadding(50, 50, 50, 50);
            textView.setText(position + 10 + "");
            return textView;
        }
    }
}
在MainActivity中給FlexibleListView添加了一個固定高度背景色為"#bbaacc"的Header,並把懸浮菜單欄actionBar賦值給了FlexibleListView的mActionBar,最後設置Adapter,為了測試代碼寫的很簡單,我們運行一下程序,看看效果:

 

\

看到運行效果好開心呀,(*^__^*) ……透明度漸變功能達到了我們的預期,接下來開始實現阻尼效果,阻尼效果就是當ListView滾動到了頂部此時若繼續下滑,ListView能夠繼續往下滾動一段距離當手指離開屏幕後ListView要恢復原位置。為了實現這個功能有的童靴可能會想到重寫有關事件傳遞的onXXXEvent()等方法,之後在MotionEvent為DOWN,MOVE,UP或者CANCEL條件下分別做邏輯判斷來實現阻尼效果,此方式可行,但是和今天我們的實現相比起來復雜了許多......

這裡所實現阻尼效果所采用的方法是利用View的overScrollBy()方法,有的童靴可能會問overScrollBy()方法是2.3版本之後才增加的,2.3版本之前的兼容性怎麼辦?我實現這個功能之前也考慮過這個問題,一方面我們公司的APP只支持3.0以上版本,另一方面2.3及以前的版本市場占有率幾乎微乎其微了,所以可以考慮不再兼容2.3以前的老版本。

有的同學或許對overScrollBy()方法比較陌生,先大致說一下該方法,其源碼如下:

/**
 * Scroll the view with standard behavior for scrolling beyond the normal
 * content boundaries. Views that call this method should override
 * {@link #onOverScrolled(int, int, boolean, boolean)} to respond to the
 * results of an over-scroll operation.
 *
 * Views can use this method to handle any touch or fling-based scrolling.
 *
 * @param deltaX Change in X in pixels
 * @param deltaY Change in Y in pixels
 * @param scrollX Current X scroll value in pixels before applying deltaX
 * @param scrollY Current Y scroll value in pixels before applying deltaY
 * @param scrollRangeX Maximum content scroll range along the X axis
 * @param scrollRangeY Maximum content scroll range along the Y axis
 * @param maxOverScrollX Number of pixels to overscroll by in either direction
 *          along the X axis.
 * @param maxOverScrollY Number of pixels to overscroll by in either direction
 *          along the Y axis.
 * @param isTouchEvent true if this scroll operation is the result of a touch event.
 * @return true if scrolling was clamped to an over-scroll boundary along either
 *          axis, false otherwise.
 */
@SuppressWarnings({"UnusedParameters"})
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, 
    int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
    // ......
}
閱讀源碼看注釋很重要,我們先看一下注釋,大致意思如下:

 

當View組件滾動到邊界時還會繼續進行之前的滾動操作(注意:沒有滾動到邊界時是不會觸發該方法的),如果View組件調用了該方法那麼View組件就應該重寫onOverScrolled()方法來響應over-scroll操作。View控件可以調用該方法處理任何的觸摸滾動或者是快速滑動等。感覺翻譯的好別扭,說的直白點就是當ListView,ScrollView等滾動到頭了若繼續下滑就會調用該方法。

overScrollBy()方法有9個參數,每個參數注釋都說的很詳細,我們只看需要用到的倆參數deltaY和isTouchEvent;deltaY表示的是在Y軸上滾動的相對值,比如ListView滾動到了頂部此時如果繼續下拉,deltaY值為負數,當其滾動到了最底部當我們繼續上拉,deltaY值為正數,所以我們可以根據deltaY判斷ListView是上拉操作還是下拉操作,isTouchEvent為true表示手指在觸摸屏幕否則離開屏幕。

了解overScrollBy()方法後開始實現阻尼效果,核心就是重寫overScrollBy()方法,在該方法中動態改變HeaderView的高度,若手指松開我們就復原HeaderView。我們知道QQ空間頂部是一張圖片,當下拉的時候該圖片有彈性拉升效果,當手指松開後圖片又伸縮回去了,所以我們就直接用ImageView模擬此效果。模擬圖片阻尼可以讓ImageView的寬高為MATCH_PARENT(HeaderView的高度改變之後ImageView的高度也可以隨之更改),這個時候還要設置ImageView的scaleType為CENTER_CROP(不清楚ImageView的scaleType屬性可參照我之前寫的一篇博文:Android 源碼系列之<一>從源碼的角度深入理解ImageView的ScaleType屬性)。

現在開始在FlexibleListView中重寫overScrollBy()方法,代碼如下:

@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 / 3.0);
            mHeaderView.requestLayout();
        }
    }
    return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
overScrollBy()方法中我們根據deltaY值動態的更改了mHeaderView的高度並重新布局達到更改ImageView高度的目的,注意:計算高度的時候用了deltaY除以3,此時的3表示增長因子,目的是讓HeaderView緩慢的增長,這裡可以對外提供一個方法來設置此值。

現在僅實現了HeaderView的拉升功能,但是還沒有實現縮放功能,因為overScrollBay()中實現的是手指觸摸的下拉,當手指離開屏幕後要進行HeaderView的復原操作,所以我們可以在考慮在onTouchEvent()方法中判斷MotionEvent的類型,當為UP或者CANCEL時就復原HeaderView,復原HeaderView不能一下子復原而是要用動畫的方式,這樣看上去才比較自然,所以onTouchEvent()代碼如下:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if(null != mHeaderView) {
        int action = ev.getAction();
        if(MotionEvent.ACTION_UP == action || MotionEvent.ACTION_CANCEL == action) {
            resetHeaderViewHeight();
        }
    }
    return super.onTouchEvent(ev);
}

private void resetHeaderViewHeight() {
    ValueAnimator valueAnimator = ValueAnimator.ofInt(1);
    valueAnimator.setDuration(700);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            final float f = animation.getAnimatedFraction();
            mHeaderView.getLayoutParams().height -= f * (mHeaderView.getLayoutParams().height - mMaxScrollHeight);
            mHeaderView.requestLayout();

        }
    });
    valueAnimator.setInterpolator(new OvershootInterpolator());
    valueAnimator.start();
}
HeaderView的復原動畫我們采用了ValueAnimator,當動畫執行過程中我們動態的更改HeaderView的值來達到漸變效果。接下來布局HeaderView來模擬QQ空間,代碼如下:

<framelayout android:layout_height="@dimen/header_height" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android">

    

    

        

        

        

        

        

        

        

    
</framelayout>
HeaderView的布局中讓ImageView的寬高都設置成了match_parent並且把scaleType設置為centerCrop。修改MainActivity的initGlobalParams()方法,代碼如下:
void initGlobalParams() {
    mListView = (FlexibleListView) findViewById(R.id.flexible_list_view);
    View mFlexibleHeaderView = LayoutInflater.from(this).inflate(R.layout.flexible_header_layout, mListView, false);
    AbsListView.LayoutParams params = (AbsListView.LayoutParams)mFlexibleHeaderView.getLayoutParams();
    if(null == params) {
        params = new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT);
    }
    params.height = getResources().getDimensionPixelSize(R.dimen.header_height);
    mFlexibleHeaderView.setLayoutParams(params);

    final View actionBar = findViewById(R.id.custom_action_bar);

    mListView.bindActionBar(actionBar);
    mListView.addHeaderView(mFlexibleHeaderView);

    mListView.setAdapter(new Adapter());
}
OK,一切都准備就緒,趕緊運行一下程序,看看效果吧(*^__^*) ……

\

恩,看上去效果還不錯......

好了,有關實現QQ空間的阻尼下拉刷新和漸變菜單欄就結束了,主要是利用了2.3版本之後的overScrollBy()方法(如果要兼容2.3之前版本需要童靴們自己去實現相關邏輯);其次充分的利用了ImageView的ScaleType屬性來模擬了QQ空間圖片阻尼回彈的效果。再次感謝收看(*^__^*) ……

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved