編輯:關於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的實現原理就完全的搞明白了,以後如果做滾動類的自定義控件,應該也有思路了。
拖了這麼久才開始更新csdn,著實是懶到家了,寫這篇博客的目的就是為了幫助更多的android入門開發者更多的了解自定義控件,畢竟自定義控件對新手來說還是比較
最近有一段時間沒寫博客了,一方面是工作比較忙,一方面也著實本人水平有限,沒有太多能與大家分享的東西,也就是在最近公司要做一個搶紅包的功能,老板發話了咋們就開干呗,本人就開
Eclipse裡有很多界面組件,文件列表、編輯區、類結構等等,在這麼多界面組件裡,再打開一個Logcat就基本沒有什麼空間了。與其擠在一起還不如分開成兩個窗口。或者你
本文實例講述了Android控件之TabHost用法。分享給大家供大家參考。具體如下:以下通過TabHost實現android選項卡。main.xml布局文件:<&