【Android界面实现】XListView实现原理讲解及分析
发表时间:2020-10-19
发布人:葵宇科技
浏览次数:23
转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992
XListview是一个异常受迎接的下拉刷新控件,然则已经停止保护了。之前写过一篇XListview的应用介绍,用起来异常简单,这两天放假无聊,研究了下XListview的实现道理,学到了很多,今天禀享给大年夜家。
提前声明,为了让代码更好的懂得,我对代码进行了部分删减和重构,如不雅大年夜家想看原版代码,请去github自行下载。
Xlistview项目主如果三部分:XlistView,XListViewHeader,XListViewFooter,分别是XListView主体、header、footer的实现。下面我们分开来介绍。
下面是修改之后的XListViewHeader代码
public class XListViewHeader extends LinearLayout { private static final String HINT_NORMAL = "下拉刷新"; private static final String HINT_READY = "松开刷新数据"; private static final String HINT_LOADING = "正在加载..."; // 正常状况 public final static int STATE_NORMAL = 0; // 预备刷新状况,也就是箭头偏向产生改变之后的状况 public final static int STATE_READY = 1; // 刷新状况,箭头变成了progressBar public final static int STATE_REFRESHING = 2; // 构造容器,也就是根构造 private LinearLayout container; // 箭头图片 private ImageView mArrowImageView; // 刷新状况显示 private ProgressBar mProgressBar; // 解释文本 private TextView mHintTextView; // 记录当前的状况 private int mState; // 用于改变箭头的偏向的动画 private Animation mRotateUpAnim; private Animation mRotateDownAnim; // 动画持续时光 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; // 初始情况下,设置下拉刷新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); } // 设置header的状况 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,用来实现下拉刷新时的界面展示,可以分为三种状况:正常、预备刷新、正在加载。
在Linearlayout构造琅绫擎,重要有指导箭头、解释文本、圆形加载条三个控件。在构造函数中,调用了initView()进行控件的初始化操作。在添加构造文件的时刻,指定高度为0,这是为了隐蔽header,然后初始化动画,是为了完成箭头的扭迁移转变作。
setState()是设置header的状况,因为header须要根据不合的状况,完成控件隐蔽、显示、改变文字等操作,这个办法主如果在XListView琅绫擎调用。除此之外,还有setVisiableHeight()和getVisiableHeight(),这两个办法是为了设置和获取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=http://www.sjsjw.com/100/000238MYM031158/"@drawable/xlistview_arrow" />
说完了Header,我们再看看Footer。Footer是为了完成加载更多功能时刻的界面展示,根本思路和Header是一样的,下面是Footer的代码
public class XListViewFooter extends LinearLayout { // 正常状况 public final static int STATE_NORMAL = 0; // 预备状况 public final static int STATE_READY = 1; // 加载状况 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); } /** * 设置当前的状况 * * @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的拉伸和显示效不雅不是经由过程高度来模仿的,而是经由过程设置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的代码实现了。
在介绍代码实现之前,我先介绍一下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的核心实现道理。
再次提示,下面的代码经由我重构了,只是为了看起来更好的懂得。
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坐标 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预备状况 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: // 记录按下的坐标 lastY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: // 计算移动距离 float deltaY = ev.getRawY() - lastY; lastY = ev.getRawY(); // 是第一项并且标题已经显示或者是鄙人拉 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() { // 松手之后调用 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()); // 未处于刷新状况,更新箭头 if (enableRefresh && !isRefreashing) { if (headerView.getVisiableHeight() > headerHeight) { headerView.setState(XListViewHeader.STATE_READY); } else { headerView.setState(XListViewHeader.STATE_NORMAL); } } } private void resetHeaderHeight() { // 当前的可见高度 int height = headerView.getVisiableHeight(); // 如不雅正在刷新并且高度没有完全展示 if ((isRefreashing && height <= headerHeight) || (height == 0)) { return; } // 默认会回滚到header的地位 int finalHeight = 0; // 如不雅是正在刷新状况,则回滚到header的高度 if (isRefreashing && height > headerHeight) { finalHeight = headerHeight; } mScrollBack = SCROLLBACK_HEADER; // 回滚到指定地位 scroller.startScroll(0, height, 0, finalHeight - height, SCROLL_DURATION); // 触发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(); } }
在三个构造函数中,都调用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的状况,
private void updateHeaderHeight(float delta) { headerView.setVisiableHeight((int) delta + headerView.getVisiableHeight()); // 未处于刷新状况,更新箭头 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); }在移开手指之后,会调用下面的resetXXX来初始化header和footer的状况
private void resetHeaderHeight() { // 当前的可见高度 int height = headerView.getVisiableHeight(); // 如不雅正在刷新并且高度没有完全展示 if ((isRefreashing && height <= headerHeight) || (height == 0)) { return; } // 默认会回滚到header的地位 int finalHeight = 0; // 如不雅是正在刷新状况,则回滚到header的高度 if (isRefreashing && height > headerHeight) { finalHeight = headerHeight; } mScrollBack = SCROLLBACK_HEADER; // 回滚到指定地位 scroller.startScroll(0, height, 0, finalHeight - height, SCROLL_DURATION); // 触发computeScroll invalidate(); } private void resetFooterHeight() { int bottomMargin = footerView.getBottomMargin(); if (bottomMargin > 0) { mScrollBack = SCROLLBACK_FOOTER; scroller.startScroll(0, bottomMargin, 0, -bottomMargin, SCROLL_DURATION); invalidate(); } }我们可以看到,滚动操作不是经由过程直接的设置高度来实现的,而是经由过程Scroller.startScroll()来实现的,经由过程调用此办法,computeScroll()就会被调用,然后在这个琅绫擎,根据mScrollBack区分是哪一个滚动,然后再经由过程设置高度和距离,就可以完成紧缩的效不雅了。
至此,全部XListView的实现道理就完全的搞明白了,今后如不雅做滚动类的自定义控件,应当也有思路了。
感谢不雅看,歇息一下。。。