XListView實現(xiàn)下拉刷新和上拉加載原理解析
XListview是一個非常受歡迎的下拉刷新控件,但是已經(jīng)停止維護了。之前寫過一篇XListview的使用介紹,用起來非常簡單,這兩天放假無聊,研究了下XListview的實現(xiàn)原理,學(xué)到了很多,今天分享給大家。
提前聲明,為了讓代碼更好的理解,我對代碼進行了部分刪減和重構(gòu),如果大家想看原版代碼,請去github自行下載。
Xlistview項目主要是三部分:XlistView,XListViewHeader,XListViewFooter,分別是XListView主體、header、footer的實現(xiàn)。下面我們分開來介紹。
下面是修改之后的XListViewHeader代碼
public class XListViewHeader extends LinearLayout {
private static final String HINT_NORMAL = "下拉刷新";
private static final String HINT_READY = "松開刷新數(shù)據(jù)";
private static final String HINT_LOADING = "正在加載...";
// 正常狀態(tài)
public final static int STATE_NORMAL = 0;
// 準(zhǔn)備刷新狀態(tài),也就是箭頭方向發(fā)生改變之后的狀態(tài)
public final static int STATE_READY = 1;
// 刷新狀態(tài),箭頭變成了progressBar
public final static int STATE_REFRESHING = 2;
// 布局容器,也就是根布局
private LinearLayout container;
// 箭頭圖片
private ImageView mArrowImageView;
// 刷新狀態(tài)顯示
private ProgressBar mProgressBar;
// 說明文本
private TextView mHintTextView;
// 記錄當(dāng)前的狀態(tài)
private int mState;
// 用于改變箭頭的方向的動畫
private Animation mRotateUpAnim;
private Animation mRotateDownAnim;
// 動畫持續(xù)時間
private final int ROTATE_ANIM_DURATION = 180;
public XListViewHeader(Context context) {
super(context);
initView(context);
}
public XListViewHeader(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
private void initView(Context context) {
mState = STATE_NORMAL;
// 初始情況下,設(shè)置下拉刷新view高度為0
LinearLayout.LayoutParams lp = new LinearLayout.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 = 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);
}
// 設(shè)置header的狀態(tài)
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(HINT_NORMAL);
break;
case STATE_READY:
if (mState != STATE_READY) {
mArrowImageView.clearAnimation();
mArrowImageView.startAnimation(mRotateUpAnim);
mHintTextView.setText(HINT_READY);
}
break;
case STATE_REFRESHING:
mHintTextView.setText(HINT_LOADING);
break;
}
mState = state;
}
public void setVisiableHeight(int height) {
if (height < 0)
height = 0;
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) container
.getLayoutParams();
lp.height = height;
container.setLayoutParams(lp);
}
public int getVisiableHeight() {
return container.getHeight();
}
public void show() {
container.setVisibility(View.VISIBLE);
}
public void hide() {
container.setVisibility(View.INVISIBLE);
}
}
XListViewHeader繼承自linearLayout,用來實現(xiàn)下拉刷新時的界面展示,可以分為三種狀態(tài):正常、準(zhǔn)備刷新、正在加載。
在Linearlayout布局里面,主要有指示箭頭、說明文本、圓形加載條三個控件。在構(gòu)造函數(shù)中,調(diào)用了initView()進行控件的初始化操作。在添加布局文件的時候,指定高度為0,這是為了隱藏header,然后初始化動畫,是為了完成箭頭的旋轉(zhuǎn)動作。
setState()是設(shè)置header的狀態(tài),因為header需要根據(jù)不同的狀態(tài),完成控件隱藏、顯示、改變文字等操作,這個方法主要是在XListView里面調(diào)用。除此之外,還有setVisiableHeight()和getVisiableHeight(),這兩個方法是為了設(shè)置和獲取Header中根布局文件的高度屬性,從而完成拉伸和收縮的效果,而show()和hide()則顯然就是完成顯示和隱藏的效果。
下面是Header的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom" >
<RelativeLayout
android:id="@+id/xlistview_header_content"
android:layout_width="match_parent"
android:layout_height="60dp"
tools:ignore="UselessParent" >
<TextView
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" />
<ImageView
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" />
<ProgressBar
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" />
</RelativeLayout>
</LinearLayout>
說完了Header,我們再看看Footer。Footer是為了完成加載更多功能時候的界面展示,基本思路和Header是一樣的,下面是Footer的代碼
public class XListViewFooter extends LinearLayout {
// 正常狀態(tài)
public final static int STATE_NORMAL = 0;
// 準(zhǔn)備狀態(tài)
public final static int STATE_READY = 1;
// 加載狀態(tài)
public final static int STATE_LOADING = 2;
private View mContentView;
private View mProgressBar;
private TextView mHintView;
public XListViewFooter(Context context) {
super(context);
initView(context);
}
public XListViewFooter(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
private void initView(Context context) {
LinearLayout moreView = (LinearLayout) LayoutInflater.from(context)
.inflate(R.layout.xlistview_footer, null);
addView(moreView);
moreView.setLayoutParams(new LinearLayout.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);
}
/**
* 設(shè)置當(dāng)前的狀態(tài)
*
* @param state
*/
public void setState(int state) {
mProgressBar.setVisibility(View.INVISIBLE);
mHintView.setVisibility(View.INVISIBLE);
switch (state) {
case STATE_READY:
mHintView.setVisibility(View.VISIBLE);
mHintView.setText(R.string.xlistview_footer_hint_ready);
break;
case STATE_NORMAL:
mHintView.setVisibility(View.VISIBLE);
mHintView.setText(R.string.xlistview_footer_hint_normal);
break;
case STATE_LOADING:
mProgressBar.setVisibility(View.VISIBLE);
break;
}
}
public void setBottomMargin(int height) {
if (height > 0) {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
.getLayoutParams();
lp.bottomMargin = height;
mContentView.setLayoutParams(lp);
}
}
public int getBottomMargin() {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
.getLayoutParams();
return lp.bottomMargin;
}
public void hide() {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
.getLayoutParams();
lp.height = 0;
mContentView.setLayoutParams(lp);
}
public void show() {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContentView
.getLayoutParams();
lp.height = LayoutParams.WRAP_CONTENT;
mContentView.setLayoutParams(lp);
}
}
從上面的代碼里面,我們可以看出,footer和header的思路是一樣的,只不過,footer的拉伸和顯示效果不是通過高度來模擬的,而是通過設(shè)置BottomMargin來完成的。
下面是Footer的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<RelativeLayout
android:id="@+id/xlistview_footer_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="5dp"
tools:ignore="UselessParent" >
<ProgressBar
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" />
<TextView
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" />
</RelativeLayout>
</LinearLayout>
在了解了Header和footer之后,我們就要介紹最核心的XListView的代碼實現(xiàn)了。
在介紹代碼實現(xiàn)之前,我先介紹一下XListView的實現(xiàn)原理。
首先,一旦使用XListView,F(xiàn)ooter和Header就已經(jīng)添加到我們的ListView上面了,XListView就是通過繼承ListView,然后處理了屏幕點擊事件和控制滑動實現(xiàn)效果的。所以,如果我們的Adapter中g(shù)etCount()返回的值是20,那么其實XListView里面是有20+2個item的,這個數(shù)量即使我們關(guān)閉了XListView的刷新和加載功能,也是不會變化的。Header和Footer通過addHeaderView和addFooterView添加上去之后,如果想實現(xiàn)下拉刷新和上拉加載功能,那么就必須有拉伸效果,所以就像上面的那樣,Header是通過設(shè)置height,F(xiàn)ooter是通過設(shè)置BottomMargin來模擬拉伸效果。那么回彈效果呢?僅僅通過設(shè)置高度或者是間隔是達不到模擬回彈效果的,因此,就需要用Scroller來實現(xiàn)模擬回彈效果。在說明原理之后,我們開始介紹XListView的核心實現(xiàn)原理。
再次提示,下面的代碼經(jīng)過我重構(gòu)了,只是為了看起來更好的理解。
public class XListView extends ListView {
private final static int SCROLLBACK_HEADER = 0;
private final static int SCROLLBACK_FOOTER = 1;
// 滑動時長
private final static int SCROLL_DURATION = 400;
// 加載更多的距離
private final static int PULL_LOAD_MORE_DELTA = 100;
// 滑動比例
private final static float OFFSET_RADIO = 2f;
// 記錄按下點的y坐標(biāo)
private float lastY;
// 用來回滾
private Scroller scroller;
private IXListViewListener mListViewListener;
private XListViewHeader headerView;
private RelativeLayout headerViewContent;
// header的高度
private int headerHeight;
// 是否能夠刷新
private boolean enableRefresh = true;
// 是否正在刷新
private boolean isRefreashing = false;
// footer
private XListViewFooter footerView;
// 是否可以加載更多
private boolean enableLoadMore;
// 是否正在加載
private boolean isLoadingMore;
// 是否footer準(zhǔn)備狀態(tài)
private boolean isFooterAdd = false;
// total list items, used to detect is at the bottom of listview.
private int totalItemCount;
// 記錄是從header還是footer返回
private int mScrollBack;
private static final String TAG = "XListView";
public XListView(Context context) {
super(context);
initView(context);
}
public XListView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public XListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context);
}
private void initView(Context context) {
scroller = new Scroller(context, new DecelerateInterpolator());
headerView = new XListViewHeader(context);
footerView = new XListViewFooter(context);
headerViewContent = (RelativeLayout) headerView
.findViewById(R.id.xlistview_header_content);
headerView.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
headerHeight = headerViewContent.getHeight();
getViewTreeObserver()
.removeGlobalOnLayoutListener(this);
}
});
addHeaderView(headerView);
}
@Override
public void setAdapter(ListAdapter adapter) {
// 確保footer最后添加并且只添加一次
if (isFooterAdd == false) {
isFooterAdd = true;
addFooterView(footerView);
}
super.setAdapter(adapter);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
totalItemCount = getAdapter().getCount();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄按下的坐標(biāo)
lastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// 計算移動距離
float deltaY = ev.getRawY() - lastY;
lastY = ev.getRawY();
// 是第一項并且標(biāo)題已經(jīng)顯示或者是在下拉
if (getFirstVisiblePosition() == 0
&& (headerView.getVisiableHeight() > 0 || deltaY > 0)) {
updateHeaderHeight(deltaY / OFFSET_RADIO);
} else if (getLastVisiblePosition() == totalItemCount - 1
&& (footerView.getBottomMargin() > 0 || deltaY < 0)) {
updateFooterHeight(-deltaY / OFFSET_RADIO);
}
break;
case MotionEvent.ACTION_UP:
if (getFirstVisiblePosition() == 0) {
if (enableRefresh
&& headerView.getVisiableHeight() > headerHeight) {
isRefreashing = true;
headerView.setState(XListViewHeader.STATE_REFRESHING);
if (mListViewListener != null) {
mListViewListener.onRefresh();
}
}
resetHeaderHeight();
} else if (getLastVisiblePosition() == totalItemCount - 1) {
if (enableLoadMore
&& footerView.getBottomMargin() > PULL_LOAD_MORE_DELTA) {
startLoadMore();
}
resetFooterHeight();
}
break;
}
return super.onTouchEvent(ev);
}
@Override
public void computeScroll() {
// 松手之后調(diào)用
if (scroller.computeScrollOffset()) {
if (mScrollBack == SCROLLBACK_HEADER) {
headerView.setVisiableHeight(scroller.getCurrY());
} else {
footerView.setBottomMargin(scroller.getCurrY());
}
postInvalidate();
}
super.computeScroll();
}
public void setPullRefreshEnable(boolean enable) {
enableRefresh = enable;
if (!enableRefresh) {
headerView.hide();
} else {
headerView.show();
}
}
public void setPullLoadEnable(boolean enable) {
enableLoadMore = enable;
if (!enableLoadMore) {
footerView.hide();
footerView.setOnClickListener(null);
} else {
isLoadingMore = false;
footerView.show();
footerView.setState(XListViewFooter.STATE_NORMAL);
footerView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startLoadMore();
}
});
}
}
public void stopRefresh() {
if (isRefreashing == true) {
isRefreashing = false;
resetHeaderHeight();
}
}
public void stopLoadMore() {
if (isLoadingMore == true) {
isLoadingMore = false;
footerView.setState(XListViewFooter.STATE_NORMAL);
}
}
private void updateHeaderHeight(float delta) {
headerView.setVisiableHeight((int) delta
+ headerView.getVisiableHeight());
// 未處于刷新狀態(tài),更新箭頭
if (enableRefresh && !isRefreashing) {
if (headerView.getVisiableHeight() > headerHeight) {
headerView.setState(XListViewHeader.STATE_READY);
} else {
headerView.setState(XListViewHeader.STATE_NORMAL);
}
}
}
private void resetHeaderHeight() {
// 當(dāng)前的可見高度
int height = headerView.getVisiableHeight();
// 如果正在刷新并且高度沒有完全展示
if ((isRefreashing && height <= headerHeight) || (height == 0)) {
return;
}
// 默認會回滾到header的位置
int finalHeight = 0;
// 如果是正在刷新狀態(tài),則回滾到header的高度
if (isRefreashing && height > headerHeight) {
finalHeight = headerHeight;
}
mScrollBack = SCROLLBACK_HEADER;
// 回滾到指定位置
scroller.startScroll(0, height, 0, finalHeight - height,
SCROLL_DURATION);
// 觸發(fā)computeScroll
invalidate();
}
private void updateFooterHeight(float delta) {
int height = 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);
}
private void resetFooterHeight() {
int bottomMargin = footerView.getBottomMargin();
if (bottomMargin > 0) {
mScrollBack = SCROLLBACK_FOOTER;
scroller.startScroll(0, bottomMargin, 0, -bottomMargin,
SCROLL_DURATION);
invalidate();
}
}
private void startLoadMore() {
isLoadingMore = true;
footerView.setState(XListViewFooter.STATE_LOADING);
if (mListViewListener != null) {
mListViewListener.onLoadMore();
}
}
public void setXListViewListener(IXListViewListener l) {
mListViewListener = l;
}
public interface IXListViewListener {
public void onRefresh();
public void onLoadMore();
}
}
在三個構(gòu)造函數(shù)中,都調(diào)用initView進行了header和footer的初始化,并且定義了一個Scroller,并傳入了一個減速的插值器,為了模仿回彈效果。在initView方法里面,因為header可能還沒初始化完畢,所以通過GlobalLayoutlistener來獲取了header的高度,然后addHeaderView添加到了listview上面。
通過重寫setAdapter方法,保證Footer最后天假,并且只添加一次。
最重要的,要屬onTouchEvent了。在方法開始之前,通過getAdapter().getCount()獲取到了item的總數(shù),便于計算位置。這個操作在源代碼中是通過scrollerListener完成的,因為ScrollerListener在這里沒大有用,所以我直接去掉了,然后把位置改到了這里。如果在setAdapter里面獲取的話,只能獲取到?jīng)]有header和footer的item數(shù)量。
在ACTION_DOWN里面,進行了lastY的初始化,lastY是為了判斷移動方向的,因為在ACTION_MOVE里面,通過ev.getRawY()-lastY可以計算出手指的移動趨勢,如果>0,那么就是向下滑動,反之向上。getRowY()是獲取元Y坐標(biāo),意思就是和Window和View坐標(biāo)沒有關(guān)系的坐標(biāo),代表在屏幕上的絕對位置。然后在下面的代碼里面,如果第一項可見并且header的可見高度>0或者是向下滑動,就說明用戶在向下拉動或者是向上拉動header,也就是指示箭頭顯示的時候的狀態(tài),這時候調(diào)用了updateHeaderHeight,來更新header的高度,實現(xiàn)header可以跟隨手指動作上下移動。這里有個OFFSET_RADIO,這個值是一個移動比例,就是說,你手指在Y方向上移動400px,如果比例是2,那么屏幕上的控件移動就是400px/2=200px,可以通過這個值來控制用戶的滑動體驗。下面的關(guān)于footer的判斷與此類似,不再贅述。
當(dāng)用戶移開手指之后,ACTION_UP方法就會被調(diào)用。在這里面,只對可見位置是0和item總數(shù)-1的位置進行了處理,其實正好對應(yīng)header和footer。如果位置是0,并且可以刷新,然后當(dāng)前的header可見高度>原始高度的話,就說明用戶確實是要進行刷新操作,所以通過setState改變header的狀態(tài),如果有監(jiān)聽器的話,就調(diào)用onRefresh方法,然后調(diào)用resetHeaderHeight初始化header的狀態(tài),因為footer的操作如出一轍,所以不再贅述。但是在footer中有一個PULL_LOAD_MORE_DELTA,這個值是加載更多觸發(fā)條件的臨界值,只有footer的間隔超過這個值之后,才能夠觸發(fā)加載更多的功能,因此我們可以修改這個值來改變用戶體驗。
說到現(xiàn)在,大家應(yīng)該明白基本的原理了,其實XListView就是通過對用戶手勢的方向和距離的判斷,來動態(tài)的改變Header和Footer實現(xiàn)的功能,所以如果我們也有類似的需求,就可以參照這種思路進行自定義。
下面再說幾個比較重要的方法。
前面我們說道,在ACTION_MOVE里面,會不斷的調(diào)用下面的updateXXXX方法,來動態(tài)的改變header和fooer的狀態(tài),
private void updateHeaderHeight(float delta) {
headerView.setVisiableHeight((int) delta
+ headerView.getVisiableHeight());
// 未處于刷新狀態(tài),更新箭頭
if (enableRefresh && !isRefreashing) {
if (headerView.getVisiableHeight() > headerHeight) {
headerView.setState(XListViewHeader.STATE_READY);
} else {
headerView.setState(XListViewHeader.STATE_NORMAL);
}
}
}
private void updateFooterHeight(float delta) {
int height = 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);
}
在移開手指之后,會調(diào)用下面的resetXXX來初始化header和footer的狀態(tài)
private void resetHeaderHeight() {
// 當(dāng)前的可見高度
int height = headerView.getVisiableHeight();
// 如果正在刷新并且高度沒有完全展示
if ((isRefreashing && height <= headerHeight) || (height == 0)) {
return;
}
// 默認會回滾到header的位置
int finalHeight = 0;
// 如果是正在刷新狀態(tài),則回滾到header的高度
if (isRefreashing && height > headerHeight) {
finalHeight = headerHeight;
}
mScrollBack = SCROLLBACK_HEADER;
// 回滾到指定位置
scroller.startScroll(0, height, 0, finalHeight - height,
SCROLL_DURATION);
// 觸發(fā)computeScroll
invalidate();
}
private void resetFooterHeight() {
int bottomMargin = footerView.getBottomMargin();
if (bottomMargin > 0) {
mScrollBack = SCROLLBACK_FOOTER;
scroller.startScroll(0, bottomMargin, 0, -bottomMargin,
SCROLL_DURATION);
invalidate();
}
}
我們可以看到,滾動操作不是通過直接的設(shè)置高度來實現(xiàn)的,而是通過Scroller.startScroll()來實現(xiàn)的,通過調(diào)用此方法,computeScroll()就會被調(diào)用,然后在這個里面,根據(jù)mScrollBack區(qū)分是哪一個滾動,然后再通過設(shè)置高度和間隔,就可以完成收縮的效果了。
至此,整個XListView的實現(xiàn)原理就完全的搞明白了,以后如果做滾動類的自定義控件,應(yīng)該也有思路了。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android Room數(shù)據(jù)庫自動升級與遷移的策略
在 Android 應(yīng)用開發(fā)中,Room 是 Google 提供的一個輕量級數(shù)據(jù)庫框架,用于簡化與 SQLite 的交互,本文將介紹 Room 數(shù)據(jù)庫升級的幾種場景和常見的處理方法,包括手動遷移和自動遷移的策略,需要的朋友可以參考下2024-09-09
Android實現(xiàn)瘋狂連連看游戲之開發(fā)游戲界面(二)
這篇文章主要為大家詳細介紹了Android實現(xiàn)瘋狂連連看游戲之開發(fā)游戲界面,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03
Android利用Paint自定義View實現(xiàn)進度條控件方法示例
這篇文章主要給大家介紹了關(guān)于Android利用Paint自定義View實現(xiàn)進度條控件的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11
Android?Jetpack組件中LifeCycle作用詳細介紹
Jetpack是谷歌在Google?I/O?2017大會上發(fā)布一套幫助開發(fā)者解決Android架構(gòu)設(shè)計的方案,而Lifecycle是Jetpack?architecture下的一部分,一起來看一下Lifecycle的使用及原理分析2022-09-09

