XListView实现下拉刷新和上拉加载原理解析

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="@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 { 

  // 正常状态
  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的实现原理就完全的搞明白了,以后如果做滚动类的自定义控件,应该也有思路了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android XListView下拉刷新和上拉加载更多

    市面上有好多的类比ListView刷新数据的开源框架,如:v4包自带的SwipeRefreshLayout ,以及集ListView.GridView甚至WebView于一身的Pulltorefresh等等.前述的两个开源框架目前使用也算频繁.有兴趣的读者可以自行搜索,当然有时间一定回来对所有的使用方式做一个汇总和比较.今天介绍的这款框架,专门针对ListView做下拉刷新与上拉加载的,如果单单是ListView就显得更加简单方便易于理解. 1.首先引入xListView_lib库到自己的Dem

  • Android通过XListView实现上拉加载下拉刷新功能

    本文实例为大家分享了XListView实现上拉加载下拉刷新的具体代码,供大家参考,具体内容如下 ## 导入XListVIew第三方库文件.通过LinkedList将刷新数据插入到集合头部,将加载的数据放入集合尾部 ## private Context context; private View view; private String path; private XListView xlv; private LinkedList<Data> listData; private Handler

  • vue移动端下拉刷新和上滑加载

    本文实例为大家分享了vue移动端下拉刷新和上滑加载的具体代码,供大家参考,具体内容如下 组件 <template> <div class="mu-load-more" @touchstart="touchStart($event)" @touchmove="touchMove($event)" @touchend="touchEnd($event)"> <div class="mu-re

  • Flutter 给列表增加下拉刷新和上滑加载更多功能

    有状态组件 当 Flutter 的页面需要动态更新数据的时候,就会涉及到 UI 组件需要根据数据变化更新,此时也就意味着组件有了"状态".这就类似 React 的类组件和函数组件(只是后续 React 使用了勾子函数实现了函数组件也可以有状态).在 Flutter 中,组件也分为无状态组件(StatelessWidget)和有状态组件(StatefulWidget),一般尽量使用无状态组件.但是如果组件本身需要维护自身状态的时候,就需要使用有状态组件了.有状态组件的定义形式如下: //

  • 基于iScroll实现下拉刷新和上滑加载效果

    本文实例为大家分享了iScroll下拉刷新上滑加载展示的具体代码,供大家参考,具体内容如下 html代码: <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=devi

  • Android仿XListView支持下拉刷新和上划加载更多的自定义RecyclerView

    首先给大家展示下效果图,感觉还不错,请继续往下阅读: 下拉刷新:        上划加载        在项目更新的过程中,遇到了一个将XListView换成recyclerView的需求,而且更换完之后大体效果不能变,但是对于下拉刷新这样的效果,谷歌给出的解决方案是把RecyclerView放在一个SwipeRefreshLayout中,但是这样其实是拉下一个小圆形控件实现的,和XListView的header效果不同.在网上找了很多的别人代码,都没有实现我想要的效果,于是自己动手写了一个.

  • XListView实现下拉刷新和上拉加载原理解析

    XListview是一个非常受欢迎的下拉刷新控件,但是已经停止维护了.之前写过一篇XListview的使用介绍,用起来非常简单,这两天放假无聊,研究了下XListview的实现原理,学到了很多,今天分享给大家. 提前声明,为了让代码更好的理解,我对代码进行了部分删减和重构,如果大家想看原版代码,请去github自行下载. Xlistview项目主要是三部分:XlistView,XListViewHeader,XListViewFooter,分别是XListView主体.header.footer

  • 使用MUI框架模拟手机端的下拉刷新和上拉加载功能

    mui框架基于htm5plus的XMLHttpRequest,封装了常用的Ajax函数,支持GET.POST请求方式,支持返回json.xml.html.text.script数据类型: 本着极简的设计原则,mui提供了mui.ajax方法,并在mui.ajax方法基础上,进一步简化出最常用的mui.get().mui.getJSON().mui.post()三个方法. 套用mui官方文档的一句话:"开发者只需关心业务逻辑,实现加载更多数据即可".真的是不错的框架. 想更多的了解这个框

  • dropload.js插件下拉刷新和上拉加载使用详解

    本文实例为大家分享了dropload.js下拉刷新和上拉加载的具体代码,供大家参考,具体内容如下 第一步,下载dropload插件,dropload插件下载地址 官方文档:https://github.com/ximan/dropload 第二步,将下载好的dropload插件中的dropload.css,dropload.min.js文件引入到页面中,注意还要引入 Jquery1.7 以上 或者 Zepto 二选一,不要同时都引用,因为dropload是基于jquery实现的 第三步,将以下代

  • android使用PullToRefresh实现下拉刷新和上拉加载

    PullToRefresh是一套实现非常好的下拉刷新库,它支持: 1.ListView 2.ExpandableListView 3.GridView 4.WebView 等多种常用的需要刷新的View类型,而且使用起来也十分方便. demo实例下载 下载完成,将它导入到eclipse中,作为一个library导入到你的工程中就好了. 一.废话少说,下拉刷新Go. 1.在你的布局文件中加上你想用的View就好了,比如这儿我想用一个支持下拉 刷新的ExpandableListView <com.h

  • H5基于iScroll实现下拉刷新和上拉加载更多

    前言 前一段有个手机端的项目需要用到下拉刷新和上拉加载更多的效果,脑海里第一反映就是微博那种效果,刚开始的理解有些偏差,以为下拉也是追加数据,上拉也是追加数据,后请教同事后发现其实下拉只是刷新最新数据而已,上拉是追加数据. 使用技巧 1.引用iScroll.js, 在初始化时添加两个事件监听:touchMove.DOMContentLoaded. 2.实现iScroll插件的onScrollEnd事件, 也就是在这个事件里调用你自己的ajax方法实现数据的刷新和追加. 3.上拉加载更多请求后台时

  • Android RecyclerView实现下拉刷新和上拉加载

    RecyclerView已经出来很久了,许许多多的项目都开始从ListView转战RecyclerView,那么,上拉加载和下拉刷新是一件很有必要的事情. 在ListView上,我们可以通过自己添加addHeadView和addFootView去添加头布局和底部局实现自定义的上拉和下拉,或者使用一些第三方库来简单的集成,例如Android-pulltorefresh或者android-Ultra-Pull-to-Refresh,后者的自定义更强,但需要自己实现上拉加载. 而在下面我们将用两种方式

随机推荐