編輯:關於Android編程
一、從用戶操作角度分析源碼的組成
XListView是一個很不錯的實現了下拉及上拉刷新的listview控件,雖然已經停止維護了,但其基本功能還是被不少app在使用的。
既然要實現上拉及下拉刷新,就以下拉為例來討論一下:
首先,下拉是用戶的一個動作,用戶按住屏幕後手指下移一定距離後再抬起手指,這是listview頂端出現額外的提示內容,當移動距離達到一定條件,就允許刷新動作。同時,listview自動上移回到頂端。
考慮到這些內容,就可以想到大致的實現方法了。
1、 布局文件要分三部分(1)下拉展示的headerview (2)上拉展示的footerview (3)正文內容listview
2、 用戶上拉、下拉動作過程中view的變化通過onTouchEvent()實現,因為這時用戶有手指觸摸屏幕的動作
3、 用戶抬起手指後,headerview或footerview的回彈動作,通過scroller來實現。(這需要對scroller有一定的了解)
二、具體的細節實現
1、headerview的源碼
public class XListViewHeader extends LinearLayout {
private LinearLayout mContainer;
private ImageView mArrowImageView;
private ProgressBar mProgressBar;
private TextView mHintTextView;
private int mState = STATE_NORMAL;
private Animation mRotateUpAnim;
private Animation mRotateDownAnim;
private final int ROTATE_ANIM_DURATION = 180;
public final static int STATE_NORMAL = 0;
public final static int STATE_READY = 1;
public final static int STATE_REFRESHING = 2;
public XListViewHeader(Context context) {
super(context);
initView(context);
}
/**
* @param context
* @param attrs
*/
public XListViewHeader(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
private void initView(Context context) {
// 初始情況,設置下拉刷新view高度為0
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LayoutParams.FILL_PARENT, 0);
mContainer = (LinearLayout) LayoutInflater.from(context).inflate(
R.layout.xlistview_header, null);
addView(mContainer, lp);
setGravity(Gravity.BOTTOM);
mArrowImageView = (ImageView)findViewById(R.id.xlistview_header_arrow);
mHintTextView = (TextView)findViewById(R.id.xlistview_header_hint_textview);
mProgressBar = (ProgressBar)findViewById(R.id.xlistview_header_progressbar);
mRotateUpAnim = new RotateAnimation(0.0f, -180.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION);
mRotateUpAnim.setFillAfter(true);
mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION);
mRotateDownAnim.setFillAfter(true);
}
/**
* 更改Headerview的狀態
* @param state
*/
public void setState(int state) {
if (state == mState) return ;
if (state == STATE_REFRESHING) { // 顯示進度
mArrowImageView.clearAnimation();
mArrowImageView.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
} else { // 顯示箭頭圖片
mArrowImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
}
switch(state){
case STATE_NORMAL:
if (mState == STATE_READY) {
mArrowImageView.startAnimation(mRotateDownAnim);
}
if (mState == STATE_REFRESHING) {
mArrowImageView.clearAnimation();
}
mHintTextView.setText(R.string.xlistview_header_hint_normal);
break;
case STATE_READY:
if (mState != STATE_READY) {
mArrowImageView.clearAnimation();
mArrowImageView.startAnimation(mRotateUpAnim);
mHintTextView.setText(R.string.xlistview_header_hint_ready);
}
break;
case STATE_REFRESHING:
mHintTextView.setText(R.string.xlistview_header_hint_loading);
break;
default:
}
mState = state;
}
/**
* 更改headerview的高度
* @param height
*/
public void setVisiableHeight(int height) {
if (height < 0)
height = 0;
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContainer
.getLayoutParams();
lp.height = height;
mContainer.setLayoutParams(lp);
}
/**
* 獲取headerview的當前高度
* @return
*/
public int getVisiableHeight() {
return mContainer.getLayoutParams().height;
}
}
headerview的源碼很容易看懂,對應的布局文件是:
這裡要注意以下,這個布局的對齊方式是bottom並且初始化其高度為0.
footerview與headerview很相似,只是其顯示不是通過更改高度,而是更改margin來實現的。
2、整個XListView的布局形成的代碼
/**
* 初始化上下拉刷新時顯示的header和footer view 和 scroller
* @param context
*/
private void initWithContext(Context context) {
mScroller = new Scroller(context, new DecelerateInterpolator());
// XListView need the scroll event, and it will dispatch the event to
// user's listener (as a proxy).
super.setOnScrollListener(this);
// init header view
mHeaderView = new XListViewHeader(context);
mHeaderViewContent = (RelativeLayout) mHeaderView
.findViewById(R.id.xlistview_header_content);
mHeaderTimeView = (TextView) mHeaderView
.findViewById(R.id.xlistview_header_time);
addHeaderView(mHeaderView);
// init footer view
mFooterView = new XListViewFooter(context);
// init header height
mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mHeaderViewHeight = mHeaderViewContent.getHeight();
getViewTreeObserver()
.removeGlobalOnLayoutListener(this);
}
});
}
@Override
public void setAdapter(ListAdapter adapter) {
// make sure XListViewFooter is the last footer view, and only add once.
if (mIsFooterReady == false) {
mIsFooterReady = true;
addFooterView(mFooterView);
}
super.setAdapter(adapter);
}
listveiw本來就有在頭部和尾部添加item的方法addHeaderview和addFooterview。
可見,headerview是在初始化時通過addHeaderview添加進去的,而footerview是在setAdapter之前添加進去並保證了其唯一性。(這裡,不是很清楚為什麼。希望研究比較深入的各位給予指點)
同時,這裡也初始化了一個scroller類,在其構造函數中還傳入了一個插值器作為參數。這樣,如果headerview或footerview高度不為零了,用戶抬起手指時再調用各自高度的reset函數,使用startscroll來配置一下這個scroller,就可以實現回彈效果了。
作者這裡用的很巧妙:
一般我們用scroller實現滑動,都是在computeScroll中調用scrollTo()這個方法並不斷的刷新view。如果這樣做能實現回彈效果,但是headerview和footerview的高度還沒有改變,而且這兩個view其實也是整個listview的兩個item。
所以作者是這樣實現的:
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
if (mScrollBack == SCROLLBACK_HEADER) {
mHeaderView.setVisiableHeight(mScroller.getCurrY());
} else {
mFooterView.setBottomMargin(mScroller.getCurrY());
}
postInvalidate();
invokeOnScrolling();
}
super.computeScroll();
}
代碼中通過scroller.computeScrollOffset不斷的重新計算CurrX和CurrY,有在內部將其設置為這兩個item的高度。
我還沒有弄清楚的是:下面這個接口的作用
/**
* you can listen ListView.OnScrollListener or this one. it will invoke
* onXScrolling when header/footer scroll back.
*/
public interface OnXScrollListener extends OnScrollListener {
public void onXScrolling(View view);
}
對XListView的細節理解可以參考下面的文章:http://blog.csdn.net/zhaokaiqiang1992/article/details/42392731
文章內容:
XListview是一個非常受歡迎的下拉刷新控件,但是已經停止維護了。之前寫過一篇XListview的使用介紹,用起來非常簡單,這兩天放假無聊,研究了下XListview的實現原理,學到了很多,今天分享給大家。
提前聲明,為了讓代碼更好的理解,我對代碼進行了部分刪減和重構,如果大家想看原版代碼,請去github自行下載。
Xlistview項目主要是三部分:XlistView,XListViewHeader,XListViewFooter,分別是XListView主體、header、footer的實現。下面我們分開來介紹。
下面是修改之後的XListViewHeader代碼
publicclassXListViewHeaderextendsLinearLayout{
privatestaticfinalStringHINT_NORMAL="下拉刷新";
privatestaticfinalStringHINT_READY="松開刷新數據";
privatestaticfinalStringHINT_LOADING="正在加載...";
//正常狀態
publicfinalstaticintSTATE_NORMAL=0;
//准備刷新狀態,也就是箭頭方向發生改變之後的狀態
publicfinalstaticintSTATE_READY=1;
//刷新狀態,箭頭變成了progressBar
publicfinalstaticintSTATE_REFRESHING=2;
//布局容器,也就是根布局
privateLinearLayoutcontainer;
//箭頭圖片
privateImageViewmArrowImageView;
//刷新狀態顯示
privateProgressBarmProgressBar;
//說明文本
privateTextViewmHintTextView;
//記錄當前的狀態
privateintmState;
//用於改變箭頭的方向的動畫
privateAnimationmRotateUpAnim;
privateAnimationmRotateDownAnim;
//動畫持續時間
privatefinalintROTATE_ANIM_DURATION=180;
publicXListViewHeader(Contextcontext){
super(context);
initView(context);
}
publicXListViewHeader(Contextcontext,AttributeSetattrs){
super(context,attrs);
initView(context);
}
privatevoidinitView(Contextcontext){
mState=STATE_NORMAL;
//初始情況下,設置下拉刷新view高度為0
LinearLayout.LayoutParamslp=newLinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT,0);
container=(LinearLayout)LayoutInflater.from(context).inflate(
R.layout.xlistview_header,null);
addView(container,lp);
//初始化控件
mArrowImageView=(ImageView)findViewById(R.id.xlistview_header_arrow);
mHintTextView=(TextView)findViewById(R.id.xlistview_header_hint_textview);
mProgressBar=(ProgressBar)findViewById(R.id.xlistview_header_progressbar);
//初始化動畫
mRotateUpAnim=newRotateAnimation(0.0f,-180.0f,
Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,
0.5f);
mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION);
mRotateUpAnim.setFillAfter(true);
mRotateDownAnim=newRotateAnimation(-180.0f,0.0f,
Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,
0.5f);
mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION);
mRotateDownAnim.setFillAfter(true);
}
//設置header的狀態
publicvoidsetState(intstate){
if(state==mState)
return;
//顯示進度
if(state==STATE_REFRESHING){
mArrowImageView.clearAnimation();
mArrowImageView.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
}else{
//顯示箭頭
mArrowImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
}
switch(state){
caseSTATE_NORMAL:
if(mState==STATE_READY){
mArrowImageView.startAnimation(mRotateDownAnim);
}
if(mState==STATE_REFRESHING){
mArrowImageView.clearAnimation();
}
mHintTextView.setText(HINT_NORMAL);
break;
caseSTATE_READY:
if(mState!=STATE_READY){
mArrowImageView.clearAnimation();
mArrowImageView.startAnimation(mRotateUpAnim);
mHintTextView.setText(HINT_READY);
}
break;
caseSTATE_REFRESHING:
mHintTextView.setText(HINT_LOADING);
break;
}
mState=state;
}
publicvoidsetVisiableHeight(intheight){
if(height<0)
height=0;
LinearLayout.LayoutParamslp=(LinearLayout.LayoutParams)container
.getLayoutParams();
lp.height=height;
container.setLayoutParams(lp);
}
publicintgetVisiableHeight(){
returncontainer.getHeight();
}
publicvoidshow(){
container.setVisibility(View.VISIBLE);
}
publicvoidhide(){
container.setVisibility(View.INVISIBLE);
}
}
XListViewHeader繼承自linearLayout,用來實現下拉刷新時的界面展示,可以分為三種狀態:正常、准備刷新、正在加載。
在Linearlayout布局裡面,主要有指示箭頭、說明文本、圓形加載條三個控件。在構造函數中,調用了initView()進行控件的初始化操作。在添加布局文件的時候,指定高度為0,這是為了隱藏header,然後初始化動畫,是為了完成箭頭的旋轉動作。
setState()是設置header的狀態,因為header需要根據不同的狀態,完成控件隱藏、顯示、改變文字等操作,這個方法主要是在XListView裡面調用。除此之外,還有setVisiableHeight()和getVisiableHeight(),這兩個方法是為了設置和獲取Header中根布局文件的高度屬性,從而完成拉伸和收縮的效果,而show()和hide()則顯然就是完成顯示和隱藏的效果。
下面是Header的布局文件
android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="bottom"> android:id="@+id/xlistview_header_content" android:layout_width="match_parent" android:layout_height="60dp" tools:ignore="UselessParent"> android:id="@+id/xlistview_header_hint_textview" android:layout_width="100dp" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="center" android:text="正在加載" android:textColor="@android:color/black" android:textSize="14sp"/> android:id="@+id/xlistview_header_arrow" android:layout_width="30dp" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toLeftOf="@id/xlistview_header_hint_textview" android:src="@drawable/xlistview_arrow"/> android:id="@+id/xlistview_header_progressbar" style="@style/progressbar_style" android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_toLeftOf="@id/xlistview_header_hint_textview" android:visibility="invisible"/>
說完了Header,我們再看看Footer。Footer是為了完成加載更多功能時候的界面展示,基本思路和Header是一樣的,下面是Footer的代碼
publicclassXListViewFooterextendsLinearLayout{
//正常狀態
publicfinalstaticintSTATE_NORMAL=0;
//准備狀態
publicfinalstaticintSTATE_READY=1;
//加載狀態
publicfinalstaticintSTATE_LOADING=2;
privateViewmContentView;
privateViewmProgressBar;
privateTextViewmHintView;
publicXListViewFooter(Contextcontext){
super(context);
initView(context);
}
publicXListViewFooter(Contextcontext,AttributeSetattrs){
super(context,attrs);
initView(context);
}
privatevoidinitView(Contextcontext){
LinearLayoutmoreView=(LinearLayout)LayoutInflater.from(context)
.inflate(R.layout.xlistview_footer,null);
addView(moreView);
moreView.setLayoutParams(newLinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT));
mContentView=moreView.findViewById(R.id.xlistview_footer_content);
mProgressBar=moreView.findViewById(R.id.xlistview_footer_progressbar);
mHintView=(TextView)moreView
.findViewById(R.id.xlistview_footer_hint_textview);
}
/**
*設置當前的狀態
*
*@paramstate
*/
publicvoidsetState(intstate){
mProgressBar.setVisibility(View.INVISIBLE);
mHintView.setVisibility(View.INVISIBLE);
switch(state){
caseSTATE_READY:
mHintView.setVisibility(View.VISIBLE);
mHintView.setText(R.string.xlistview_footer_hint_ready);
break;
caseSTATE_NORMAL:
mHintView.setVisibility(View.VISIBLE);
mHintView.setText(R.string.xlistview_footer_hint_normal);
break;
caseSTATE_LOADING:
mProgressBar.setVisibility(View.VISIBLE);
break;
}
}
publicvoidsetBottomMargin(intheight){
if(height>0){
LinearLayout.LayoutParamslp=(LinearLayout.LayoutParams)mContentView
.getLayoutParams();
lp.bottomMargin=height;
mContentView.setLayoutParams(lp);
}
}
publicintgetBottomMargin(){
LinearLayout.LayoutParamslp=(LinearLayout.LayoutParams)mContentView
.getLayoutParams();
returnlp.bottomMargin;
}
publicvoidhide(){
LinearLayout.LayoutParamslp=(LinearLayout.LayoutParams)mContentView
.getLayoutParams();
lp.height=0;
mContentView.setLayoutParams(lp);
}
publicvoidshow(){
LinearLayout.LayoutParamslp=(LinearLayout.LayoutParams)mContentView
.getLayoutParams();
lp.height=LayoutParams.WRAP_CONTENT;
mContentView.setLayoutParams(lp);
}
}
從上面的代碼裡面,我們可以看出,footer和header的思路是一樣的,只不過,footer的拉伸和顯示效果不是通過高度來模擬的,而是通過設置BottomMargin來完成的。
android:layout_width="fill_parent" android:layout_height="wrap_content"> android:id="@+id/xlistview_footer_content" android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="5dp" tools:ignore="UselessParent"> android:id="@+id/xlistview_footer_progressbar" style="@style/progressbar_style" android:layout_width="30dp" android:layout_height="30dp" android:layout_centerInParent="true" android:visibility="invisible"/> android:id="@+id/xlistview_footer_hint_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/xlistview_footer_hint_normal" android:textColor="@android:color/black" android:textSize="14sp"/>
在了解了Header和footer之後,我們就要介紹最核心的XListView的代碼實現了。
在介紹代碼實現之前,我先介紹一下XListView的實現原理。
首先,一旦使用XListView,Footer和Header就已經添加到我們的ListView上面了,XListView就是通過繼承ListView,然後處理了屏幕點擊事件和控制滑動實現效果的。所以,如果我們的Adapter中getCount()返回的值是20,那麼其實XListView裡面是有20+2個item的,這個數量即使我們關閉了XListView的刷新和加載功能,也是不會變化的。Header和Footer通過addHeaderView和addFooterView添加上去之後,如果想實現下拉刷新和上拉加載功能,那麼就必須有拉伸效果,所以就像上面的那樣,Header是通過設置height,Footer是通過設置BottomMargin來模擬拉伸效果。那麼回彈效果呢?僅僅通過設置高度或者是間隔是達不到模擬回彈效果的,因此,就需要用Scroller來實現模擬回彈效果。在說明原理之後,我們開始介紹XListView的核心實現原理。
再次提示,下面的代碼經過我重構了,只是為了看起來更好的理解。
publicclassXListViewextendsListView{
privatefinalstaticintSCROLLBACK_HEADER=0;
privatefinalstaticintSCROLLBACK_FOOTER=1;
//滑動時長
privatefinalstaticintSCROLL_DURATION=400;
//加載更多的距離
privatefinalstaticintPULL_LOAD_MORE_DELTA=100;
//滑動比例
privatefinalstaticfloatOFFSET_RADIO=2f;
//記錄按下點的y坐標
privatefloatlastY;
//用來回滾
privateScrollerscroller;
privateIXListViewListenermListViewListener;
privateXListViewHeaderheaderView;
privateRelativeLayoutheaderViewContent;
//header的高度
privateintheaderHeight;
//是否能夠刷新
privatebooleanenableRefresh=true;
//是否正在刷新
privatebooleanisRefreashing=false;
//footer
privateXListViewFooterfooterView;
//是否可以加載更多
privatebooleanenableLoadMore;
//是否正在加載
privatebooleanisLoadingMore;
//是否footer准備狀態
privatebooleanisFooterAdd=false;
//totallistitems,usedtodetectisatthebottomoflistview.
privateinttotalItemCount;
//記錄是從header還是footer返回
privateintmScrollBack;
privatestaticfinalStringTAG="XListView";
publicXListView(Contextcontext){
super(context);
initView(context);
}
publicXListView(Contextcontext,AttributeSetattrs){
super(context,attrs);
initView(context);
}
publicXListView(Contextcontext,AttributeSetattrs,intdefStyle){
super(context,attrs,defStyle);
initView(context);
}
privatevoidinitView(Contextcontext){
scroller=newScroller(context,newDecelerateInterpolator());
headerView=newXListViewHeader(context);
footerView=newXListViewFooter(context);
headerViewContent=(RelativeLayout)headerView
.findViewById(R.id.xlistview_header_content);
headerView.getViewTreeObserver().addOnGlobalLayoutListener(
newOnGlobalLayoutListener(){
@SuppressWarnings("deprecation")
@Override
publicvoidonGlobalLayout(){
headerHeight=headerViewContent.getHeight();
getViewTreeObserver()
.removeGlobalOnLayoutListener(this);
}
});
addHeaderView(headerView);
}
@Override
publicvoidsetAdapter(ListAdapteradapter){
//確保footer最後添加並且只添加一次
if(isFooterAdd==false){
isFooterAdd=true;
addFooterView(footerView);
}
super.setAdapter(adapter);
}
@Override
publicbooleanonTouchEvent(MotionEventev){
totalItemCount=getAdapter().getCount();
switch(ev.getAction()){
caseMotionEvent.ACTION_DOWN:
//記錄按下的坐標
lastY=ev.getRawY();
break;
caseMotionEvent.ACTION_MOVE:
//計算移動距離
floatdeltaY=ev.getRawY()-lastY;
lastY=ev.getRawY();
//是第一項並且標題已經顯示或者是在下拉
if(getFirstVisiblePosition()==0
&&(headerView.getVisiableHeight()>0||deltaY>0)){
updateHeaderHeight(deltaY/OFFSET_RADIO);
}elseif(getLastVisiblePosition()==totalItemCount-1
&&(footerView.getBottomMargin()>0||deltaY<0)){
updateFooterHeight(-deltaY/OFFSET_RADIO);
}
break;
caseMotionEvent.ACTION_UP:
if(getFirstVisiblePosition()==0){
if(enableRefresh
&&headerView.getVisiableHeight()>headerHeight){
isRefreashing=true;
headerView.setState(XListViewHeader.STATE_REFRESHING);
if(mListViewListener!=null){
mListViewListener.onRefresh();
}
}
resetHeaderHeight();
}elseif(getLastVisiblePosition()==totalItemCount-1){
if(enableLoadMore
&&footerView.getBottomMargin()>PULL_LOAD_MORE_DELTA){
startLoadMore();
}
resetFooterHeight();
}
break;
}
returnsuper.onTouchEvent(ev);
}
@Override
publicvoidcomputeScroll(){
//松手之後調用
if(scroller.computeScrollOffset()){
if(mScrollBack==SCROLLBACK_HEADER){
headerView.setVisiableHeight(scroller.getCurrY());
}else{
footerView.setBottomMargin(scroller.getCurrY());
}
postInvalidate();
}
super.computeScroll();
}
publicvoidsetPullRefreshEnable(booleanenable){
enableRefresh=enable;
if(!enableRefresh){
headerView.hide();
}else{
headerView.show();
}
}
publicvoidsetPullLoadEnable(booleanenable){
enableLoadMore=enable;
if(!enableLoadMore){
footerView.hide();
footerView.setOnClickListener(null);
}else{
isLoadingMore=false;
footerView.show();
footerView.setState(XListViewFooter.STATE_NORMAL);
footerView.setOnClickListener(newOnClickListener(){
@Override
publicvoidonClick(Viewv){
startLoadMore();
}
});
}
}
publicvoidstopRefresh(){
if(isRefreashing==true){
isRefreashing=false;
resetHeaderHeight();
}
}
publicvoidstopLoadMore(){
if(isLoadingMore==true){
isLoadingMore=false;
footerView.setState(XListViewFooter.STATE_NORMAL);
}
}
privatevoidupdateHeaderHeight(floatdelta){
headerView.setVisiableHeight((int)delta
+headerView.getVisiableHeight());
//未處於刷新狀態,更新箭頭
if(enableRefresh&&!isRefreashing){
if(headerView.getVisiableHeight()>headerHeight){
headerView.setState(XListViewHeader.STATE_READY);
}else{
headerView.setState(XListViewHeader.STATE_NORMAL);
}
}
}
privatevoidresetHeaderHeight(){
//當前的可見高度
intheight=headerView.getVisiableHeight();
//如果正在刷新並且高度沒有完全展示
if((isRefreashing&&height<=headerHeight)||(height==0)){
return;
}
//默認會回滾到header的位置
intfinalHeight=0;
//如果是正在刷新狀態,則回滾到header的高度
if(isRefreashing&&height>headerHeight){
finalHeight=headerHeight;
}
mScrollBack=SCROLLBACK_HEADER;
//回滾到指定位置
scroller.startScroll(0,height,0,finalHeight-height,
SCROLL_DURATION);
//觸發computeScroll
invalidate();
}
privatevoidupdateFooterHeight(floatdelta){
intheight=footerView.getBottomMargin()+(int)delta;
if(enableLoadMore&&!isLoadingMore){
if(height>PULL_LOAD_MORE_DELTA){
footerView.setState(XListViewFooter.STATE_READY);
}else{
footerView.setState(XListViewFooter.STATE_NORMAL);
}
}
footerView.setBottomMargin(height);
}
privatevoidresetFooterHeight(){
intbottomMargin=footerView.getBottomMargin();
if(bottomMargin>0){
mScrollBack=SCROLLBACK_FOOTER;
scroller.startScroll(0,bottomMargin,0,-bottomMargin,
SCROLL_DURATION);
invalidate();
}
}
privatevoidstartLoadMore(){
isLoadingMore=true;
footerView.setState(XListViewFooter.STATE_LOADING);
if(mListViewListener!=null){
mListViewListener.onLoadMore();
}
}
publicvoidsetXListViewListener(IXListViewListenerl){
mListViewListener=l;
}
publicinterfaceIXListViewListener{
publicvoidonRefresh();
publicvoidonLoadMore();
}
}
在三個構造函數中,都調用initView進行了header和footer的初始化,並且定義了一個Scroller,並傳入了一個減速的插值器,為了模仿回彈效果。在initView方法裡面,因為header可能還沒初始化完畢,所以通過GlobalLayoutlistener來獲取了header的高度,然後addHeaderView添加到了listview上面。
通過重寫setAdapter方法,保證Footer最後天假,並且只添加一次。
最重要的,要屬onTouchEvent了。在方法開始之前,通過getAdapter().getCount()獲取到了item的總數,便於計算位置。這個操作在源代碼中是通過scrollerListener完成的,因為ScrollerListener在這裡沒大有用,所以我直接去掉了,然後把位置改到了這裡。如果在setAdapter裡面獲取的話,只能獲取到沒有header和footer的item數量。
在ACTION_DOWN裡面,進行了lastY的初始化,lastY是為了判斷移動方向的,因為在ACTION_MOVE裡面,通過ev.getRawY()-lastY可以計算出手指的移動趨勢,如果>0,那麼就是向下滑動,反之向上。getRowY()是獲取元Y坐標,意思就是和Window和View坐標沒有關系的坐標,代表在屏幕上的絕對位置。然後在下面的代碼裡面,如果第一項可見並且header的可見高度>0或者是向下滑動,就說明用戶在向下拉動或者是向上拉動header,也就是指示箭頭顯示的時候的狀態,這時候調用了updateHeaderHeight,來更新header的高度,實現header可以跟隨手指動作上下移動。這裡有個OFFSET_RADIO,這個值是一個移動比例,就是說,你手指在Y方向上移動400px,如果比例是2,那麼屏幕上的控件移動就是400px/2=200px,可以通過這個值來控制用戶的滑動體驗。下面的關於footer的判斷與此類似,不再贅述。
當用戶移開手指之後,ACTION_UP方法就會被調用。在這裡面,只對可見位置是0和item總數-1的位置進行了處理,其實正好對應header和footer。如果位置是0,並且可以刷新,然後當前的header可見高度>原始高度的話,就說明用戶確實是要進行刷新操作,所以通過setState改變header的狀態,如果有監聽器的話,就調用onRefresh方法,然後調用resetHeaderHeight初始化header的狀態,因為footer的操作如出一轍,所以不再贅述。但是在footer中有一個PULL_LOAD_MORE_DELTA,這個值是加載更多觸發條件的臨界值,只有footer的間隔超過這個值之後,才能夠觸發加載更多的功能,因此我們可以修改這個值來改變用戶體驗。
說到現在,大家應該明白基本的原理了,其實XListView就是通過對用戶手勢的方向和距離的判斷,來動態的改變Header和Footer實現的功能,所以如果我們也有類似的需求,就可以參照這種思路進行自定義。
下面再說幾個比較重要的方法。
前面我們說道,在ACTION_MOVE裡面,會不斷的調用下面的updateXXXX方法,來動態的改變header和fooer的狀態,
privatevoidupdateHeaderHeight(floatdelta){
headerView.setVisiableHeight((int)delta
+headerView.getVisiableHeight());
//未處於刷新狀態,更新箭頭
if(enableRefresh&&!isRefreashing){
if(headerView.getVisiableHeight()>headerHeight){
headerView.setState(XListViewHeader.STATE_READY);
}else{
headerView.setState(XListViewHeader.STATE_NORMAL);
}
}
}
privatevoidupdateFooterHeight(floatdelta){
intheight=footerView.getBottomMargin()+(int)delta;
if(enableLoadMore&&!isLoadingMore){
if(height>PULL_LOAD_MORE_DELTA){
footerView.setState(XListViewFooter.STATE_READY);
}else{
footerView.setState(XListViewFooter.STATE_NORMAL);
}
}
footerView.setBottomMargin(height);
}
privatevoidresetHeaderHeight(){
//當前的可見高度
intheight=headerView.getVisiableHeight();
//如果正在刷新並且高度沒有完全展示
if((isRefreashing&&height<=headerHeight)||(height==0)){
return;
}
//默認會回滾到header的位置
intfinalHeight=0;
//如果是正在刷新狀態,則回滾到header的高度
if(isRefreashing&&height>headerHeight){
finalHeight=headerHeight;
}
mScrollBack=SCROLLBACK_HEADER;
//回滾到指定位置
scroller.startScroll(0,height,0,finalHeight-height,
SCROLL_DURATION);
//觸發computeScroll
invalidate();
}
privatevoidresetFooterHeight(){
intbottomMargin=footerView.getBottomMargin();
if(bottomMargin>0){
mScrollBack=SCROLLBACK_FOOTER;
scroller.startScroll(0,bottomMargin,0,-bottomMargin,
SCROLL_DURATION);
invalidate();
}
}
我們可以看到,滾動操作不是通過直接的設置高度來實現的,而是通過Scroller.startScroll()來實現的,通過調用此方法,computeScroll()就會被調用,然後在這個裡面,根據mScrollBack區分是哪一個滾動,然後再通過設置高度和間隔,就可以完成收縮的效果了。
至此,整個XListView的實現原理就完全的搞明白了,以後如果做滾動類的自定義控件,應該也有思路了。
Android自定義view之環形等待控件的實現
拖了這麼久才開始更新csdn,著實是懶到家了,寫這篇博客的目的就是為了幫助更多的android入門開發者更多的了解自定義控件,畢竟自定義控件對新手來說還是比較
android搶紅包代碼解析支持微信與QQ
最近有一段時間沒寫博客了,一方面是工作比較忙,一方面也著實本人水平有限,沒有太多能與大家分享的東西,也就是在最近公司要做一個搶紅包的功能,老板發話了咋們就開干呗,本人就開
Android分屏顯示LogCat
Eclipse裡有很多界面組件,文件列表、編輯區、類結構等等,在這麼多界面組件裡,再打開一個Logcat就基本沒有什麼空間了。與其擠在一起還不如分開成兩個窗口。或者你
Android控件之TabHost用法實例分析
本文實例講述了Android控件之TabHost用法。分享給大家供大家參考。具體如下:以下通過TabHost實現android選項卡。main.xml布局文件:<&