横向ListView (二)—— 添加快速滚动功能及item相关事件实现

  • Post author:
  • Post category:其他


在读本文之前,请先阅读博文

《横向ListView(一) ——开篇,基础逻辑实现》

之前的文章已经介绍了横向lListView的基础实现逻辑,在这里我将介绍快速滚动实现及item相关事件实现

列表的快速滚动的实现主要依赖于android官方提供的android.widget.Scroller类,具体实现需要以下步骤:

1.捕获快速滑动事件,并启动快速滑动计算(Scroller的功能)

2.使用Scroller计算一次发生滚动的位移值,刷新视图

3.如果整体滑动还未停止(即Scroller的滚动计算还未结束),则重复执行步骤2

4.捕获按下事件,实现当用户按下时停止自动快速滚动操作

如对Scroller的工作原理不了解的,可以参考以下文章:


《Android Scroller完全解析,关于Scroller你所需知道的一切》


《ndroid 带你从源码的角度解析Scroller的滚动实现原理》

对于ListView,item需要响应的事件比较重要的就两个,点击和长按,具体实现如下:

1.点击事件:在OnGestureListener中添加

public boolean


onSingleTapConfirmed

(MotionEvent e)方法的实现,以响应点击事件

2.长按事件:在OnGestureListener中添加

public void


onLongPress

(MotionEvent e) 方法的实现,以响应长按事件

先上完整代码:

package com.hss.os.horizontallistview.history_version;

import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.Scroller;

import java.util.LinkedList;
import java.util.Queue;

/**
 * 为横向ListView添加快速滚动功能及item相关事件实现
 *
 * Created by sxyx on 2017/8/8.
 */

public class HorizontalListView2 extends AdapterView<ListAdapter> {

    private ListAdapter adapter = null;
    private GestureDetector mGesture;
    private Queue<View> cacheView = new LinkedList<>();//列表项缓存视图
    private int firstItemIndex = 0;//显示的第一个子项的下标
    private int lastItemIndex = -1;//显示的最后的一个子项的下标
    private int scrollValue=0;//列表已经发生有效滚动的位移值
    private int hasToScrollValue=0;//接下来列表发生滚动所要达到的位移值
    private int maxScrollValue=Integer.MAX_VALUE;//列表发生滚动所能达到的最大位移值(这个由最后显示的列表项决定)
    private int displayOffset=0;//列表显示的偏移值(用于矫正列表显示的所有子项的显示位置)

    private Scroller mScroller;
    private int firstItemLeftEdge=0;//第一个子项的左边界
    private int lastItemRightEdge=0;//最后一个子项的右边界


    public HorizontalListView2(Context context) {
        super(context);
        init(context);
    }

    public HorizontalListView2(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public HorizontalListView2(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public HorizontalListView2(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context){
        mGesture = new GestureDetector(getContext(), mOnGesture);
        mScroller=new Scroller(context);
    }



    private void initParams(){
        mScroller.forceFinished(true);//避免在滑动过程中变换视图内容时,出现列表无法滚动的情况
        removeAllViewsInLayout();
        if(adapter!=null&&lastItemIndex<adapter.getCount())
            hasToScrollValue=scrollValue;//保持显示位置不变
        else hasToScrollValue=0;//滚动到列表头
        scrollValue=0;//列表已经发生有效滚动的位移值
        firstItemIndex = 0;//显示的第一个子项的下标
        lastItemIndex = -1;//显示的最后的一个子项的下标
        maxScrollValue=Integer.MAX_VALUE;//列表发生滚动所能达到的最大位移值(这个由最后显示的列表项决定)
        displayOffset=0;//列表显示的偏移值(用于矫正列表显示的所有子项的显示位置)
        firstItemLeftEdge=0;//第一个子项的左边界
        lastItemRightEdge=0;//最后一个子项的右边界
        requestLayout();
    }


    private DataSetObserver mDataObserver = new DataSetObserver() {

        @Override
        public void onChanged() {
            //执行Adapter数据改变时的逻辑
            initParams();
        }

        @Override
        public void onInvalidated() {
            //执行Adapter数据失效时的逻辑
            initParams();
        }

    };

    @Override
    public ListAdapter getAdapter() {
        return adapter;
    }

    @Override
    public void setAdapter(ListAdapter adapter) {
        if(adapter!=null){
            adapter.registerDataSetObserver(mDataObserver);
        }
        if(this.adapter!=null){
            this.adapter.unregisterDataSetObserver(mDataObserver);
        }
        this.adapter=adapter;
        requestLayout();
    }

    @Override
    public View getSelectedView() {
        return null;
    }

    @Override
    public void setSelection(int i) {

    }

    private void addAndMeasureChild(View child, int viewIndex) {
        LayoutParams params = child.getLayoutParams();
        params = params==null ? new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT):params;

        addViewInLayout(child, viewIndex, params, true);
        child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED),
                MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED));
    }



    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        Log.e("","============>>>>>left:"+left+" top:"+top+" right:"+right+" bottom:"+bottom);
        //需要先布局列表项再根据余下的空间布局列表头尾
        //布局列表项
        /*
        1.计算这一次整体滚动偏移量
        2.根据偏移量提取需要缓存视图
        3.根据偏移量显示新的列表项
        4.根据整体偏移值整顿所有列表项位置
        5.计算最大滚动位移值,记录已经发生有效滚动的位移值
        6.根据显示的最终效果,判断是否要居中显示
         */

        int dx=calculateScrollValue();
        removeNonVisibleItems(dx);
        showListItem(dx);
        adjustItems();
        calculateMaxScrollValue();

        //继续滚动
        if(!mScroller.isFinished()){
            post(new Runnable(){
                @Override
                public void run() {
                    requestLayout();
                }
            });
        }
    }

    /**
     * 计算这一次整体滚动偏移量
     * @return
     */
    private int calculateScrollValue(){
        int dx=0;

        if(mScroller.computeScrollOffset()){
            hasToScrollValue = mScroller.getCurrX();
        }

        if(hasToScrollValue<=0){
            hasToScrollValue=0;
            mScroller.forceFinished(true);
        }
        if(hasToScrollValue >= maxScrollValue) {
            hasToScrollValue = maxScrollValue;
            mScroller.forceFinished(true);
        }
        dx=hasToScrollValue-scrollValue;
        scrollValue=hasToScrollValue;

        return -dx;
    }

    /**
     * 计算最大滚动值
     */
    private void calculateMaxScrollValue(){

        if(getListItemCount()>0) {
            if(lastItemIndex==adapter.getCount()-1) {//已经显示了最后一项
                if(getChildAt(getChildCount() - 1).getRight()>=getShowEndEdge()) {
                    maxScrollValue = scrollValue + getChildAt(getChildCount() - 1).getRight() - getShowEndEdge();
                }else{
                    maxScrollValue=0;
                }
            }
        }else{
            if(adapter!=null&&adapter.getCount()>0){

            }else {
                if (getChildCount() > 0
                        && getChildAt(getChildCount() - 1).getRight() >= getShowEndEdge()) {
                    maxScrollValue = scrollValue + getChildAt(getChildCount() - 1).getRight() - getShowEndEdge();
                } else {
                    maxScrollValue = 0;
                }
            }
        }
    }

    /**
     * 根据偏移量提取需要缓存视图
     * @param dx
     */
    private void removeNonVisibleItems(int dx) {
        if(getListItemCount()>0) {
            //移除列表头
            View child = getChildAt(getStartItemIndex());
            while (getListItemCount()>0&&child != null && child.getRight() + dx <= getShowStartEdge()) {
                displayOffset += child.getMeasuredWidth();
                cacheView.offer(child);
                removeViewInLayout(child);
                firstItemIndex++;
                child = getChildAt(getStartItemIndex());
            }

            //移除列表尾
            child = getChildAt(getEndItemIndex());
            while (getListItemCount()>0&&child != null && child.getLeft() + dx >= getShowEndEdge()) {
                cacheView.offer(child);
                removeViewInLayout(child);
                lastItemIndex--;
                child = getChildAt(getEndItemIndex());
            }
        }
    }

    /**
     * 根据偏移量显示新的列表项
     * @param dx
     */
    private void showListItem(int dx) {
        if(adapter==null)return;

        int firstItemEdge = getFirstItemLeftEdge()+dx;
        int lastItemEdge = getLastItemRightEdge()+dx;
        displayOffset+=dx;//计算偏移量
        //显示列表头视图
        while(firstItemEdge > getShowStartEdge() && firstItemIndex-1 >= 0) {
            firstItemIndex--;//往前显示一个列表项
            View child = adapter.getView(firstItemIndex, cacheView.poll(), this);
            addAndMeasureChild(child, getStartItemIndex());
            firstItemEdge -= child.getMeasuredWidth();
            displayOffset -= child.getMeasuredWidth();
        }
        //显示列表未视图
        while(lastItemEdge < getShowEndEdge() && lastItemIndex+1 < adapter.getCount()) {
            lastItemIndex++;//往后显示一个列表项
            View child = adapter.getView(lastItemIndex, cacheView.poll(), this);
            addAndMeasureChild(child, getEndItemIndex()+1);
            lastItemEdge += child.getMeasuredWidth();
        }
    }

    /**
     * 调整各个item的位置
     */
    private void adjustItems() {
        if(getListItemCount() > 0){
            int left = displayOffset+getShowStartEdge();
            int top = getPaddingTop();
            int endIndex = getEndItemIndex();

            int childWidth,childHeight;
            for(int i=0;i<=endIndex;i++){
                View child = getChildAt(i);
                childWidth = child.getMeasuredWidth();
                childHeight = child.getMeasuredHeight();
                child.layout(left, top, left + childWidth, top + childHeight);
                left += childWidth;
            }

            firstItemLeftEdge=getChildAt(getStartItemIndex()).getLeft();
            lastItemRightEdge=getChildAt(getEndItemIndex()).getRight();
        }
    }

    //以下八个方法为概念性封装方法,有助于往后的扩展和维护


    /**
     * 获得列表视图中item View的总数
     * @return
     */
    private int getListItemCount(){
        int itemCount=getChildCount();
        return itemCount;
    }
    /**
     * 获得列表视图中第一个item View下标
     * @return
     */
    private int getStartItemIndex(){
        return 0;
    }
    /**
     * 获得列表视图中最后一个item View下标
     * @return
     */
    private int getEndItemIndex(){
        return getChildCount()-1;
    }
    /**
     * 获得列表视图中第一个item View左边界值
     * @return
     */
    private int getFirstItemLeftEdge(){
        if(getListItemCount()>0) {
            return firstItemLeftEdge;
        }else{
            return 0;
        }
    }
    /**
     * 获得列表视图中最后一个item View右边界值
     * @return
     */
    private int getLastItemRightEdge(){
        if(getListItemCount()>0) {
            return lastItemRightEdge;
        }else{
            return 0;
        }
    }
    /**
     * 取得视图可见区域的左边界
     * @return
     */
    private int getShowStartEdge(){
        return getPaddingLeft();
    }
    /**
     * 取得视图可见区域的右边界
     * @return
     */
    private int getShowEndEdge(){
        return getWidth()-getPaddingRight();
    }
    /**
     * 取得视图可见区域的宽度
     * @return
     */
    private int getShowWidth(){
        return getWidth()-getPaddingLeft()-getPaddingRight();
    }





    /**
     * 在onTouchEvent处理事件,让子视图优先消费事件
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mGesture.onTouchEvent(event);
    }

    private GestureDetector.OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() {

        @Override
        public boolean onDown(MotionEvent e) {
            mScroller.forceFinished(true);//点击时停止滚动
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                               float velocityY) {
            mScroller.fling(hasToScrollValue, 0, (int)-velocityX, 0, 0, maxScrollValue, 0, 0);
            requestLayout();
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                                float distanceX, float distanceY) {

            synchronized(HorizontalListView2.this){
                hasToScrollValue += (int)distanceX;
            }
            requestLayout();
            return true;
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            for(int i=0;i<getChildCount();i++){
                View child = getChildAt(i);
                if (isEventWithinView(e, child)) {
                    int position=firstItemIndex + i;
                    if (getOnItemClickListener() != null) {
                        getOnItemClickListener().onItemClick(HorizontalListView2.this, child, position, adapter.getItemId(position));
                    }
                    if (getOnItemSelectedListener() != null) {
                        getOnItemSelectedListener().onItemSelected(HorizontalListView2.this, child, position, adapter.getItemId(position));
                    }
                    break;
                }

            }
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (isEventWithinView(e, child)) {
                    int position=firstItemIndex + i;
                    if (getOnItemLongClickListener() != null) {
                        getOnItemLongClickListener().onItemLongClick(HorizontalListView2.this, child, position, adapter.getItemId(position));
                    }
                    break;
                }

            }
        }

        private boolean isEventWithinView(MotionEvent e, View child) {
            Rect viewRect = new Rect();
            int[] childPosition = new int[2];
            child.getLocationOnScreen(childPosition);
            int left = childPosition[0];
            int right = left + child.getWidth();
            int top = childPosition[1];
            int bottom = top + child.getHeight();
            viewRect.set(left, top, right, bottom);
            return viewRect.contains((int) e.getRawX(), (int) e.getRawY());
        }
    };

    public synchronized void scrollTo(int x) {
        mScroller.startScroll(hasToScrollValue, 0, x - hasToScrollValue, 0);
        requestLayout();
    }

}

列表的快速滚动的实现主要依赖于android官方提供的android.widget.Scroller类,具体实现需要以下步骤:

1.捕获快速滑动事件,并启动快速滑动计算(Scroller的功能)

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                       float velocityY) {
    mScroller.fling(hasToScrollValue, 0, (int)-velocityX, 0, 0, maxScrollValue, 0, 0);
    requestLayout();
    return true;
}

在OnGestureListener中添加

public boolean


onFling

(MotionEvent e1

,

MotionEvent e2

, float

velocityX

,


float

velocityY)方法,以捕获快速滑动事件,调用Scroller.fling()启动快速滑动计算,然后调用requestLayout()要求重新布局界面,已实现快速滑动效果。

2.使用Scroller计算一次发生滚动的位移值,刷新视图

在未实现快速滑动之前

calculateScrollValue

()方法实现如下:

/**
 * 计算这一次整体滚动偏移量
 * @return
 */
private int calculateScrollValue(){
    int dx=0;
    hasToScrollValue=hasToScrollValue<0? 0:hasToScrollValue;
    hasToScrollValue=hasToScrollValue>maxScrollValue? maxScrollValue:hasToScrollValue;
    dx=hasToScrollValue-scrollValue;
    scrollValue=hasToScrollValue;

    return -dx;
}

以下代码是为了实现快速滑动而做出的修改:

/**
 * 计算这一次整体滚动偏移量
 * @return
 */
private int calculateScrollValue(){
    int dx=0;

    if(mScroller.computeScrollOffset()){
        hasToScrollValue = mScroller.getCurrX();
    }

    if(hasToScrollValue<=0){
        hasToScrollValue=0;
        mScroller.forceFinished(true);
    }
    if(hasToScrollValue >= maxScrollValue) {
        hasToScrollValue = maxScrollValue;
        mScroller.forceFinished(true);
    }
    dx=hasToScrollValue-scrollValue;
    scrollValue=hasToScrollValue;

    return -dx;
}

主要是调用Scroller计算需要发生滚动的位移值,以及在滚动到边界上的时候,让Scroller停止计算

3.如果整体滑动还未停止(即Scroller的滚动计算还未结束),则重复执行步骤2

这一步只需要在

protected void


onLayout

(

boolean

changed

, int

left

, int

top

, int

right

, int

bottom)方法中添加以下代码即可

//继续滚动
if(!mScroller.isFinished()){
    post(new Runnable(){
        @Override
        public void run() {
            requestLayout();
        }
    });
}

Scroller的整体滚动计算还未完成则调用 requestLayout()不断重复刷新界面,直到整体滚动完成

4.捕获按下事件,实现当用户按下时停止自动快速滚动操作

@Override
public boolean onDown(MotionEvent e) {
    mScroller.forceFinished(true);//点击时停止滚动
    return true;
}

在OnGestureListener的

public boolean


onDown(

MotionEvent e

)

方法中添加

mScroller

.forceFinished(

true

)

;

用于告诉Scroller整体滚动计算需要停止,借此停止整个界面的滑动

对于ListView,item需要响应的事件比较重要的就两个,点击和长按,具体实现如下:

1.点击事件:在OnGestureListener中添加

public boolean


onSingleTapConfirmed

(MotionEvent e)方法的实现,以响应点击事件

@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
    for(int i=0;i<getChildCount();i++){
        View child = getChildAt(i);
        if (isEventWithinView(e, child)) {
            int position=firstItemIndex + i;
            if (getOnItemClickListener() != null) {
                getOnItemClickListener().onItemClick(HorizontalListView2.this, child, position, adapter.getItemId(position));
            }
            if (getOnItemSelectedListener() != null) {
                getOnItemSelectedListener().onItemSelected(HorizontalListView2.this, child, position, adapter.getItemId(position));
            }
            break;
        }

    }
    return true;
}

2.长按事件:在OnGestureListener中添加

public void


onLongPress

(MotionEvent e) 方法的实现,以响应长按事件

@Override
public void onLongPress(MotionEvent e) {
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (isEventWithinView(e, child)) {
            int position=firstItemIndex + i;
            if (getOnItemLongClickListener() != null) {
                getOnItemLongClickListener().onItemLongClick(HorizontalListView2.this, child, position, adapter.getItemId(position));
            }
            break;
        }

    }
}


转载于:https://my.oschina.net/u/3614895/blog/1504435