在读本文之前,请先阅读博文《横向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;
}
}
}