XListView如何实现下拉刷新和上拉加载

这篇文章主要介绍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的布局文件

 
 
 
   
 
     
 
     
 
     
   
 

    说完了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的布局文件 

 
 
 
   
 
     
 
     
   
 

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

以上是“XListView如何实现下拉刷新和上拉加载”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注创新互联行业资讯频道!


网站名称:XListView如何实现下拉刷新和上拉加载
当前路径:http://hbruida.cn/article/joedph.html