1Requirement
- 有一个竖向滑动的列表和一个表头,表头每列对应显示列表项;
- 列表的每行从第二列开始可以横向滚动,列表头的每项也可以横向滚动;
- 表头和每行滚动一致
2 Effect Picture
3 Demo
4 Theory
- ListView的addHeaderView实现列表加表头
- 重写ListView的触摸事件,监听按下、抬起、移动、惯性滑动事件,定义VelocityTracker获取手指滑动速度
- 将滚动事件的参数封装到一个ScrollerCompat中去
- 表头和表项包含一个可以滚动的自定义控件
自定义控件获取ScrollerCompat,设置位置
注意:记录滚动的位置,ListView向下滑动时,itemView要设置到相应位置
5 CoreCode
(1) 监听ListView的滚动事件,获取事件类型、元素(方向、速度、距离)
/**
* Created by Ray on 2018/5/10.
* 获取手势:方向、速度、距离
*/
public class HSListView extends ListView {
public HSListView(Context context) {
super(context);
}
public HSListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HSListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private HScrollListViewListener mListener;
/**
* 紀錄手指按下時的x座標
*/
private float xDown;
/**
* 紀錄手指按下時的y座標
*/
private float yDown;
/**
* 紀錄手指移動時的x坐標
*/
private float xMove;
/**
* 紀錄手指移動時的y座標
*/
private float yMove;
/**
* 紀錄手指放開時的x座標
*/
private float xUp;
/**
* 紀錄手指放開時的y座標
*/
private float yUp;
/**
* 計算手指滑動的速度
*/
private VelocityTracker mVelocityTracker;
/**
* X軸滑動到這個距離觸發水平滑動,暫停垂直滑動
* <li>預設30
*/
private int touchXSlop = 30;
/**
* Y軸滑動到這個距離觸發垂直滑動,暫停水平滑動
* <li>預設30
*/
private int touchYSlop = 30;
private int REBOUND_DISTANCE_X = 30; //scroller X軸反彈效果的距離
private int distanceY;
private boolean isSliding, isPulling;
public void setOnListener(HScrollListViewListener listener) {
mListener = listener;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
createVelocityTracker(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDown = ev.getRawX();
yDown = ev.getRawY();
xMove = xDown;
yMove = yDown;
isSliding = false;
isPulling = false;
if (mListener != null) {
mListener.onTouchDown(ev.getX(), ev.getY());
}
break;
case MotionEvent.ACTION_MOVE:
//先判斷是否上下滑動,若上下滑動就鎖死左右滑動
if (!isSliding && Math.abs(yDown - ev.getRawY()) >= touchYSlop) {
isSliding = false;
isPulling = true;
}
//若不是上下滑動,就判斷是否左右滑動,是的話就鎖死上下滑動
if (!isPulling && Math.abs(xDown - ev.getRawX()) >= touchXSlop) {
isSliding = true;
isPulling = false;
}
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return super.onInterceptTouchEvent(ev);
case MotionEvent.ACTION_MOVE:
if (isSliding && !isPulling) {
return true;
} else {
return super.onInterceptTouchEvent(ev);
}
case MotionEvent.ACTION_UP:
if (isSliding && !isPulling) {
return true;
} else {
return super.onInterceptTouchEvent(ev);
}
default:
super.onInterceptTouchEvent(ev);
break;
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return super.onTouchEvent(event);
case MotionEvent.ACTION_MOVE:
int moveDistanceX = (int) (xMove - event.getRawX());
distanceY = (int) (yMove - event.getRawY());
xMove = event.getRawX();
yMove = event.getRawY();
if (mListener != null && isSliding && !isPulling && xDown > 0 ) {
mListener.onSliding(moveDistanceX);
return true;
} else {
return super.onTouchEvent(event);
}
case MotionEvent.ACTION_UP:
xUp = event.getX();
yUp = event.getY();
if (mListener != null) {
mListener.onTouchUp(xUp, yUp);
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
if (mListener != null && isSliding && xDown > 0 ) {
mListener.onFling(0, 0, -getScrollVelocity() * 2 / 3, 0, 0, 0, 0, 0, REBOUND_DISTANCE_X, 0);
isSliding = false;
isPulling = false;
recycleVelocityTracker();
return true;
} else {
return super.onTouchEvent(event);
}
default:
super.onTouchEvent(event);
break;
}
return true;
}
/**
* 獲取手指在右邊layout的監聽View上的滑動速度
* @return 滑動速度,以每秒鐘移動了多少像素為單位
*/
private int getScrollVelocity() {
mVelocityTracker.computeCurrentVelocity(1000);
return (int) mVelocityTracker.getXVelocity();
}
/**
* 回收VelocityTracker
*/
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void createVelocityTracker(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
public interface HScrollListViewListener {
void onTouchDown(float x, float y);
void onSliding(int moveDistanceX);
void onFling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY);
void onTouchUp(float x, float y);
}
}
(2)可以滚动的自定义控件
/**
* Created by Ray on 2018/5/10.
* 横向滚动的表头和List 的item中可以滚动的组件
*/
public class HScrollLayout extends LinearLayout {
private ScrollerCompat mScrollerCompat;
public HScrollLayout(Context context) {
super(context);
}
public HScrollLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public HScrollLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScrollerCompat != null && mScrollerCompat.computeScrollOffset()) {
scrollTo(mScrollerCompat.getCurrX(), 0);
postInvalidate();
}
}
public void setScroller(ScrollerCompat scroller) {
mScrollerCompat = scroller;
}
}
(3)实现监听手势,设置UI
scrollerCompat = ScrollerCompat.create(this);
hsl.setScroller(scrollerCompat);
mAdapter.setScroller(scrollerCompat);
lv.setOnListener(new HSListView.HScrollListViewListener() {
@Override
public void onTouchDown(float x, float y) {
scrollXPos = scrollerCompat.getCurrX();
scrollerCompat.abortAnimation();
if (scrollXPos < 0) {
scrollXPos = 0;
} else if (scrollXPos >= hScrollMaxWidth - UtilsDensity.dp2px(getApplicationContext(), 3)) {
scrollXPos = hScrollMaxWidth;
}
setViewPosition(scrollXPos);
}
@Override
public void onSliding(int moveDistanceX) {
scrollXPos = scrollXPos + moveDistanceX;
if (scrollXPos < 0) {
scrollXPos = 0;
} else if (scrollXPos > hScrollMaxWidth) {
scrollXPos = hScrollMaxWidth;
}
setViewPosition(scrollXPos);
}
@Override
public void onFling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
if (hsl.getScrollX() <= 0 && velocityX <= 0) {
scrollerCompat.fling(0, startY, 0, velocityY, minX, hScrollMaxWidth, minY, maxY, overX, overY);
} else if (hsl.getScrollX() >= hScrollMaxWidth && velocityX >= 0) {
scrollerCompat.fling(hScrollMaxWidth, startY, 0, velocityY, minX, hScrollMaxWidth, minY, maxY, overX, overY);
} else {
scrollerCompat.fling(hsl.getScrollX(), startY, velocityX, velocityY, minX, hScrollMaxWidth, minY, maxY, overX, overY);
}
hsl.postInvalidate();
for (int i = 0; i < lv.getChildCount(); i++) {
if (lv.getChildAt(i).findViewById(R.id.hsl) != null) {
lv.getChildAt(i).findViewById(R.id.hsl).postInvalidate();
}
}
scrollXPos = scrollerCompat.getFinalX();
if (mAdapter != null) {
mAdapter.setScrollXPos(scrollXPos);
}
}
@Override
public void onTouchUp(float x, float y) {
scrollXPos = hsl.getScrollX();
if (scrollXPos < 0) {
scrollXPos = 0;
} else if (scrollXPos > hScrollMaxWidth) {
scrollXPos = hScrollMaxWidth;
}
setViewPosition(scrollXPos);
}
private void setViewPosition(int distance){
hsl.scrollTo(distance, 0);
for (int i = 0; i < lv.getChildCount(); i++) {
if (lv.getChildAt(i).findViewById(R.id.hsl) != null) {
lv.getChildAt(i).findViewById(R.id.hsl).scrollTo(distance, 0);
}
}
if (mAdapter != null) {
mAdapter.setScrollXPos(distance);
}
}
});
(4)item实现滚动
@Override
public View getView(int position, View convertView, ViewGroup parent) {
...
HScrollLayout hScrollLayout = convertView.findViewById(R.id.hsl);
if(hScrollLayout != null && mScrollerCompat != null) {
hScrollLayout.setScroller(mScrollerCompat);
}
if (scrollXPos != 0) {
hScrollLayout.scrollTo(scrollXPos,0);
} else {
if(mScrollerCompat != null) {
hScrollLayout.scrollTo(mScrollerCompat.getCurrX(), 0);
}
}
return convertView;
}
public void setScrollXPos(int pos){
this.scrollXPos = pos;
}