一言不合先上图:
本文实现上图效果采用了两种方案:
方案一:采用头布局+左右两个ListView实现,头布局的右半部分以及右边的ListView完全包含在自定义HorizontalScrollView中。
首先自定义一个CusHorizontalScrollView,在其中声明另一个HorizontalScrollView mView,添加set方法,复写onScrollChanged()方法,当本身滚动时,调用mView的滚动方法,以实现头布局和列表布局的水平同步滚动。代码如下:
private HorizontalScrollView mView;
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if(mView != null){
mView.scrollTo(l, t);
}
}
public void setSynHorizontalScrollView(HorizontalScrollView horizontalScrollView){
mView = horizontalScrollView;
}
本方案除了考虑头布局右半部分和列表item右半部分水平方向上同步滚动外,还需要考虑两个ListView在竖直方向上同步滚动的问题,解决方案如下:
/**
* 两个listView同步滑动
* @param listView1
* @param listView2
*/
public static void setListViewOnTouchAndScrollListener(final ListView listView1, final ListView listView2){
//设置listview2列表的scroll监听,用于滑动过程中左右不同步时校正
listView2.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//如果停止滑动
if(scrollState == 0 || scrollState == 1){
//获取第一个子view
View subView = view.getChildAt(0);
if(subView != null){
int top = subView.getTop(); //第一个可见item的顶部距离ListView顶部的距离,通常为负值
int top1 = listView1.getChildAt(0).getTop();
int position = view.getFirstVisiblePosition();
//Log.i("sty", "content top: " + top + " | title top:: " + top1 + " | position: " + position );
//如果两个首个显示的子view高度不等
if(top != top1){
listView1.setSelectionFromTop(position, top);
}
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
View subView = view.getChildAt(0);
View otherView = listView1.getChildAt(0);
if(subView != null && otherView != null){
int top = subView.getTop();
int top1 = otherView.getTop();
//Log.i("sty", "-----content top: " + top + " | title top:: " + top1 + " | firstVisibleItem: " + firstVisibleItem );
//如果两个首个显示的子view高度不等
if(!(top1 - 7 < top && top < top1 + 7)){
listView1.setSelectionFromTop(firstVisibleItem, top);
listView2.setSelectionFromTop(firstVisibleItem, top);
}
//使用以下代码可以解决两个listView同步不流畅的问题,但是应该会更耗性能
// if(top != top1){
// listView1.setSelectionFromTop(firstVisibleItem, top);
// listView2.setSelectionFromTop(firstVisibleItem, top);
// }
}
}
});
//设置listview1列表的scroll监听,用于滑动过程中左右不同步时校正
listView1.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//如果停止滑动
if(scrollState == 0 || scrollState == 1){
//获取第一个子view
View subView = view.getChildAt(0);
View otherView = listView2.getChildAt(0);
if(subView != null){
int top = subView.getTop();
int top1 = otherView.getTop();
int position = view.getFirstVisiblePosition();
//如果两个首个显示的子view高度不等
if(top != top1){
listView1.setSelectionFromTop(position, top);
listView2.setSelectionFromTop(position, top);
}
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
View subView = view.getChildAt(0);
if(subView != null){
int top = subView.getTop();
listView1.setSelectionFromTop(firstVisibleItem, top);
listView2.setSelectionFromTop(firstVisibleItem, top);
}
}
});
}
使用代码如下:
private void initView(){
lvName = findViewById(R.id.lv_name);
lvContent = findViewById(R.id.lv_content);
hslHeader = findViewById(R.id.hsl_header);
hslContent = findViewById(R.id.hsl_content);
lvName.setTag(lvContent);
lvContent.setTag(lvName);
hslHeader.setSynHorizontalScrollView(hslContent);
hslContent.setSynHorizontalScrollView(hslHeader);
ViewUtils.setListViewOnTouchAndScrollListener(lvName, lvContent);
}
private void setListView(){
dataList = createDataList();
setLvName();
setLvContent();
lvName.setSelection(0);
lvContent.setSelection(0);
}
方案二:采用头布局+一个ListView实现,头布局的右半部分以及ListView中的Item的右半部分包含在自定义的HorizontalScrollView中。
首先自定义一个CustomHScrollView,在其中定义一个滚动监听器OnScrollChangedListener和滚动观察者ScrollViewObserver,该滚动观察者持有一个滚动监听器的list集合,在滚动观察者的notifyOnScrollChanged()方法中遍历其持有的滚动监听器,并调用监听器的回调方法。然后复写HorizontalScrollView中的onScrollChanged()方法,在其中调用滚动观察者的notifyOnScrollChanged()方法,以实现对ListView中所有item的自定义CustomHScrollView实现同步滚动。代码如下:
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
//滚动时通知观察者
if(mScrollViewObserver != null){
mScrollViewObserver.notifyOnScrollChanged(l, t, oldl, oldt);
}
super.onScrollChanged(l, t, oldl, oldt);
}
/**
* 自定义的滚动监听接口
* 当发生滚动事件时的接口,供外部访问
*/
public interface OnScrollChangedListener{
void onScrollChanged(int l, int t, int oldl, int oldt);
}
//添加滚动事件监听
public void addOnScrollChangedListener(OnScrollChangedListener listener){
mScrollViewObserver.addOnScrollChangedListener(listener);
}
//移除滚动事件监听
public void removeOnScrollChangedListener(OnScrollChangedListener listener){
mScrollViewObserver.removeOnScrollChangedListener(listener);
}
/**
* 滚动观察者
*/
public static class ScrollViewObserver{
List<OnScrollChangedListener> mChangedListeners;
public ScrollViewObserver(){
super();
mChangedListeners = new ArrayList<>();
}
//添加滚动事件监听
public void addOnScrollChangedListener(OnScrollChangedListener listener){
mChangedListeners.add(listener);
}
//移除滚动事件监听
public void removeOnScrollChangedListener(OnScrollChangedListener listener){
mChangedListeners.remove(listener);
}
//通知
public void notifyOnScrollChanged(int l, int t, int oldl, int oldt){
if(mChangedListeners == null || mChangedListeners.size() == 0){
return;
}
for(int i = 0; i < mChangedListeners.size(); i++){
if(mChangedListeners.get(i) != null){
mChangedListeners.get(i).onScrollChanged(l, t, oldl, oldt);
}
}
}
}
为了解决滑动冲突问题,需要在自定义的CustomHScrollView外层包一层自定义的线性布局InterceptScrollLinearLayout,在其中根据手势来做事件拦截和分发操作,核心代码如下:
//拦截onTouch事件 上下滑动时拦截事件返回true 左右滑动时不拦截返回false
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if (mTouchState == TOUCH_STATE_HORIZONTAL_SCROLLING) {
intercept = false; //左右滑动时不拦截事件
} else if (mTouchState == TOUCH_STATE_VERTICAL_SCROLLING) {
intercept = true; //上下滑动时拦截
} else {
float x = ev.getX();
int xDiff = (int) Math.abs(x - mLastMotionX);
boolean xMoved = xDiff > mTouchSlop;
float y = ev.getY();
int yDiff = (int) Math.abs(y - mLastMotionY);
boolean yMoved = yDiff > mTouchSlop;
if(xMoved){
if(xDiff >= yDiff && xDiff >= mTouchSlop){ //Scroll if the user moved far enough along the X axis
mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
mLastMotionX = x;
}
}
if(yMoved){
if(yDiff > xDiff && yDiff >= mTouchSlop){ //Scroll if the user moved far enough along the Y axis
mTouchState = TOUCH_STATE_VERTICAL_SCROLLING;
mLastMotionY = y;
}
}
}
break;
case MotionEvent.ACTION_DOWN:
mTouchState = TOUCH_STATE_REST;
mLastMotionX = ev.getX();
mLastMotionY = ev.getY();
intercept = false;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//release the drag
mTouchState = TOUCH_STATE_REST;
intercept = false;
break;
default:
break;
}
return intercept;
}
列表适配器初始化时需要传入头布局,在getView()方法中需要找到头布局中的自定义CustomHScrollView headerScrollView,以该headerScrollView为基准,为其添加每个item得到的CustomHScrollView滚动回调监听器,当item的CustomHScrollView滚动时只需要调用headerScrollView的滚动事件,就可以实现头布局和所有item水平同步滚动了。核心代码如下:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if(convertView == null){
convertView = inflater.inflate(R.layout.list_item, parent, false);
holder = new ViewHolder();
CustomHScrollView scrollView = convertView.findViewById(R.id.hsl_scrollview);
holder.scrollView = scrollView;
//省略部分获取控件的代码
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
//省略部分给控件设置值的代码
final CustomHScrollView headerScrollview = listHeader.findViewById(R.id.hsl_scrollview);
headerScrollview.addOnScrollChangedListener(new OnScrollChangedListenerImpl(holder.scrollView));
holder.scrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View view, int i, int i1, int i2, int i3) {
headerScrollview.smoothScrollTo(i, i1);
}
});
holder.tvName.setOnClickListener(new MyOnClickListener(position));
holder.llItemList.setOnClickListener(new MyOnClickListener(position));
return convertView;
}
class OnScrollChangedListenerImpl implements CustomHScrollView.OnScrollChangedListener{
CustomHScrollView mScrollViewArg;
public OnScrollChangedListenerImpl(CustomHScrollView scrollViewArg){
mScrollViewArg = scrollViewArg;
}
@Override
public void onScrollChanged(int l, int t, int oldl, int oldt) {
mScrollViewArg.smoothScrollTo(l, t);
}
}
使用代码如下:
private void initView() {
mListView = findViewById(R.id.lv_list_view);
listViewHeader = findViewById(R.id.head_layout);
listViewHeader.setFocusable(true);
listViewHeader.setClickable(true);
setListView();
}
private void setListView(){
dataList = createDataList();
adapter = new ListViewAdapter(this, dataList, listViewHeader);
mListView.setAdapter(adapter);
}
源码传送门:https://github.com/tianyalu/RelationListView
如果觉得本文对您有帮助的话欢迎给个star。
本文参考:Android -- 自定义实现横竖双向滚动的列表(ListView)布局
Android开发ScrollView上下左右滑动事件冲突整理一(根据事件)
在此对各位大神表示感谢。