一起撸个朋友圈吧 - ListView(中)篇

项目地址:https://github.com/razerdp/FriendCircle

上一篇我们初步弄出了一个Header,虽然这个header实现的仅仅是弄了一个灰色的图层,但我们需要的是它的回调。

这一篇,我们针对框架封装一个listview出来。

这里简要说说_android_-Ultra-Pull-To-_Refresh_这个框架,这个框架继承viewgroup,其实现原理是只能够add2个view,一个作为header,一个作为content,事件分发在dispatchTouchEvent处理,由于继承的viewgroup,所以理论上来说可以添加任何view来实现下拉刷新。

那我们目的就很明确,要将这个框架弄成一个listview(起码让使用的人看起来就是一个listview),我们就要按照listview的风格去弄这个控件,首先当然是定义我们的attrs,我们的attrs属性直接拉官方的包,在as中切换到project标签,依次打开 ->res->values->attrs.xml,然后ctrl+f找到abslistview和listview,把你觉得常用的都拉到我们自己新建的attrs.xml里面。

1.png

经过筛选,初步提取出以下属性:

<?xml version="1.0" encoding="utf-8"?><resources>
    <declare-styleable name="FriendCirclePtrListView">
        <!--abslistview start-->
        <!--=====================================-->
        <!-- Drawable used to indicate the currently selected item in the list. -->
        <attr name="listSelector" format="color|reference" />
        <attr name="transcriptMode">
            <!-- Disables transcript mode. This is the default value. -->
            <enum name="disabled" value="0"/>
            <!-- The list will automatically scroll to the bottom when
                 a data set change notification is received and only if the last item is
                 already visible on screen. -->
            <enum name="normal" value="1" />
            <!-- The list will automatically scroll to the bottom, no matter what items
                 are currently visible. -->
            <enum name="alwaysScroll" value="2" />
        </attr>
        <!-- Indicates that this list will always be drawn on top of solid, single-color
            opaque background. This allows the list to optimize drawing. -->
        <attr name="cacheColorHint" format="color" />
        <!-- Enables the fast scroll thumb that can be dragged to quickly scroll through
            the list. -->
        <attr name="fastScrollEnabled" format="boolean" />
        <!-- Specifies the style of the fast scroll decorations. -->
        <attr name="fastScrollStyle" format="reference" />
        <!-- When set to true, the list will use a more refined calculation
             method based on the pixels height of the items visible on screen. This
             property is set to true by default but should be set to false if your adapter
             will display items of varying heights. When this property is set to true and
             your adapter displays items of varying heights, the scrollbar thumb will
             change size as the user scrolls through the list. When set to fale, the list
             will use only the number of items in the adapter and the number of items visible
             on screen to determine the scrollbar's properties. -->
        <attr name="smoothScrollbar" format="boolean" />
        <!-- Defines the choice behavior for the view. By default, lists do not have
           any choice behavior. By setting the choiceMode to singleChoice, the list
           allows up to one item to be in a chosen state. By setting the choiceMode to
           multipleChoice, the list allows any number of items to be chosen.
           Finally, by setting the choiceMode to multipleChoiceModal the list allows
           any number of items to be chosen in a special selection mode.
           The application will supply a
           {@link android.widget.AbsListView.MultiChoiceModeListener} using
           {@link android.widget.AbsListView#setMultiChoiceModeListener} to control the
           selection mode. This uses the {@link android.view.ActionMode} API. -->
        <attr name="choiceMode">
            <!-- Normal list that does not indicate choices. -->
            <enum name="none" value="0" />
            <!-- The list allows up to one choice. -->
            <enum name="singleChoice" value="1" />
            <!-- The list allows multiple choices. -->
            <enum name="multipleChoice" value="2" />
            <!-- The list allows multiple choices in a custom selection mode. -->
            <enum name="multipleChoiceModal" value="3" />
        </attr>
        <!--=====================================-->
        <!--abslistview end-->
        <!--=====================================-->
        <!--listview start-->
        <!-- Drawable or color to draw between list items. -->
        <attr name="listview_divider" format="reference|color" />
        <!-- Height of the divider. Will use the intrinsic height of the divider if this
             is not specified. -->
        <attr name="dividerHeight" format="dimension" />
        <!-- Drawable to draw above list content. -->
        <attr name="overScrollHeader" format="reference|color" />
        <!-- Drawable to draw below list content. -->
        <attr name="overScrollFooter" format="reference|color" />
    </declare-styleable></resources>

然后在我们的构造器中直接拉官方源码:

 private void initAttrs(Context context, AttributeSet attrs) {
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FriendCirclePtrListView);        
        final Drawable selector = a.getDrawable(R.styleable.FriendCirclePtrListView_listSelector);        
        if (selector != null) {
            mListView.setSelector(selector);
        }
        mListView.setTranscriptMode(a.getInt(R.styleable.FriendCirclePtrListView_transcriptMode, 0));
        mListView.setCacheColorHint(a.getColor(R.styleable.FriendCirclePtrListView_cacheColorHint, 0));
        mListView.setFastScrollEnabled(a.getBoolean(R.styleable.FriendCirclePtrListView_fastScrollEnabled, false));        
        
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mListView.setFastScrollStyle(a.getResourceId(R.styleable.FriendCirclePtrListView_fastScrollStyle, 0));
        }
        mListView.setSmoothScrollbarEnabled(a.getBoolean(R.styleable.FriendCirclePtrListView_smoothScrollbar, true));
        mListView.setChoiceMode(a.getInt(R.styleable.FriendCirclePtrListView_choiceMode, 0));        
        
        final Drawable d = a.getDrawable(R.styleable.FriendCirclePtrListView_listview_divider);        
        if (d != null) {            
            // Use an implicit divider height which may be explicitly
            // overridden by android:dividerHeight further down.
            mListView.setDivider(d);
        }        
        // Use an explicit divider height, if specified.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {            
            if (a.hasValueOrEmpty(R.styleable.FriendCirclePtrListView_dividerHeight)) {                
                final int dividerHeight = a.getDimensionPixelSize(R.styleable.FriendCirclePtrListView_dividerHeight, 0);                if (dividerHeight != 0) {
                mListView.setDividerHeight(dividerHeight);
                }
            }
        }        
        else {            
            final int dividerHeight = a.getDimensionPixelSize(R.styleable.FriendCirclePtrListView_dividerHeight, 0);            if (dividerHeight != 0) {
            mListView.setDividerHeight(dividerHeight);
            }
        }        
        final Drawable osHeader = a.getDrawable(R.styleable.FriendCirclePtrListView_overScrollHeader);        
        if (osHeader != null) {
            mListView.setOverscrollHeader(osHeader);
        }        
        final Drawable osFooter = a.getDrawable(R.styleable.FriendCirclePtrListView_overScrollFooter);        
        if (osFooter != null) {
            mListView.setOverscrollFooter(osFooter);
        }
        a.recycle();
    }

值得注意的是dividerheight这个属性,需要区分一下SDK版本,另外我的divider这个属性不知道为什么会提示重复属性,于是我只好改了一下名字改为listview_divider

初始化中进行各种各样的框架属性定义,代码如下:

private void initView(Context context) {        
        //header
        mHeader = new FriendCirclePtrHeader(context);        
       
        //listview
        mListView = new ListView(context);
        mListView.setSelector(android.R.color.transparent);
        mListView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));        
        
        //footer
        mFooter = new FriendCirclePtrFooter(context);        
        
        //view add
        setHeaderView(mHeader);
        addView(mListView);        
        
        //ptr option
        addPtrUIHandler(mHeader.getPtrUIHandler());
        setPtrHandler(this);
        setResistance(2.3f);
        setRatioOfHeaderHeightToRefresh(.25f);
        setDurationToClose(200);
        setDurationToCloseHeader(1000);        
        
        //刷新时的固定的偏移量
        setOffsetToKeepHeaderWhileLoading(0);        
        
        //下拉刷新,即下拉到距离就刷新而不是松开刷新
        setPullToRefresh(false);        
        
        //刷新的时候保持头部?
        setKeepHeaderWhenRefresh(false);
        setScrollListener();
    }

我们在控件中new一个listview,作为content,然后new一个header,就是上一篇的那个header,作为我们的header,接着footer备用,用于滑到底部自动加载时显示用的,这里没有什么技术含量,在setScrollListener(),我们对listview进行滑动监听,当滑动到底部的时候,进行加载更多的操作(本篇暂未实现

  int lastItem = 0;    
  
  private void setScrollListener() {
        mListView.setOnScrollListener(new AbsListView.OnScrollListener() {            
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {                
                if (mOnLoadMoreRefreshListener != null) {                    
                    if (SCROLL_STATE_IDLE == scrollState &&0 != mListView.getFirstVisiblePosition() && lastItem == mListView.getCount()) {                        
                        if (hasMore && loadmoreState != PullStatus.REFRESHING) {                            
                            // TODO: 2016/2/10 待完成
                            //当有更多同时当前加载更多布局不再刷新状态,则执行刷新
                        }
                    }
                }
            }            
            
            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                lastItem = firstVisibleItem + visibleItemCount;
            }
        });
    }

那么,现在listview有了,滑动监听也有了,我们该如何实现下拉刷新的监听呢,在框架中有这么一个接口PtrHandler,这个接口需要我们实现两个回调:

public interface PtrHandler {    
    /**
     * Check can do refresh or not. For example the content is empty or the first child is in view.
     * <p/>
     * {@link in.srain.cube.views.ptr.PtrDefaultHandler#checkContentCanBePulledDown}
     */
    public boolean checkCanDoRefresh(final PtrFrameLayout frame, final View content, final View header);    
    
    /**
     * When refresh begin
     *
     * @param frame
     */
    public void onRefreshBegin(final PtrFrameLayout frame);
}

根据官方文档,第一个回调是我们决定能否下拉,通常返回官方自带的判断工具类就可以了,第二个就是刷新回调了。

为了方便控制,我们在控件里定义两个枚举:

  • 当前模式:下拉刷新、上拉加载

  • 当前状态:普通(无状态)、正在刷新

定义这两个状态的目的是为了方便我们以后扩展的时候用,比如如果当前状态是正在刷新,我们就禁用掉下拉功能什么的。。。。

public enum PullStatus {
    NORMAL,REFRESHING
}
public enum PullMode {
    FROM_START,FROM_BOTTOM
}

同时,我们定义两个接口,这两个接口用于外部回调,方便控制状态:

/**
 * Created by 大灯泡 on 2016/2/9.
 * 下拉刷新接口
 */
 public interface OnPullDownRefreshListener {    
     void onRefreshing(PtrFrameLayout frame);
}
/**
 * Created by 大灯泡 on 2016/2/9.
 * 加载更多接口
 */
 public interface OnLoadMoreRefreshListener {    
     void onRefreshing();
}

接下来在我们的框架回调中执行下面步骤:

@Override
public void onRefreshBegin(PtrFrameLayout frame) {
    curMode = PullMode.FROM_START;
    loadmoreState = PullStatus.NORMAL;        
    if (mOnPullDownRefreshListener != null) 
        mOnPullDownRefreshListener.onRefreshing(frame);
}

根据官方文档,官方并未提供上拉加载更多的接口,也就是说这个回调必定是下拉刷新的回调,所以我们的模式指定为from_start,loadmoreState(加载更多状态)则是normal,另外还有一个pullState,这个是下拉状态,该状态由header对应ui接口回调控制。(详情看上篇)

做完这一系列的操作后,我们的下拉刷新基本完成了,但是还有一个很重要的东东,就是刷新的icon,但是这个icon我们的listview不负责控制,控制在header里面(详情看上篇),listview仅用于传值。

在中篇最后让我们分析一下:

到目前为止:

  • 我们写了一个header,一个listview(继承PtrFrameLayout)

  • 其中:

  • header有两个作用,一个是控制自身下拉的展示,另一个是控制刷新icon的展示

  • listview则是继承框架,其作用是做刷新相关操作以及暴露listview接口,让外界看起来像是一个listview

写到这里我思考到一个问题:刷新icon,listview,header这三者的耦合度是不是有点太高了

另外,关于icon使用margintop来更新是否会重复导致measure和layout的问题,在我的测试打印日志里面没有发生。

关于这个问题,待我查查官方资料,以及思考一下,在下篇讨论一下。