右边的listview分好组以后,在左边的Tab页建立索引。可以直接导航,是不是很方便。关键在于右边滑动,左边也会跟着滑;而点击左边呢,也能定位右边的项。它们存在这样一种特殊的交互。像这种联动的效果,还有些常见的例子呢,比如知乎采用了常见的toolbar+viewPager的联动,只不过是上下布局:
再看看点评,它的城市选择页面也有这种联动的影子,只是稍微弱一点。侧边栏可以对listview进行索引,这最早是在微信好友列表里出现的把:
趁着周末,我也撸一个。就拓展性而言,应该可以适配以上所有情况吧。我称其为LinkedLayout,看下效果图:
我把右边按5个一组,可以看到,左边的索引 = 右边/5
特点
右边滑动,左边跟着动
左边滑动到边界,右边跟着动
点击左边tab项,右边滑动定位到相应的group
源码
github 传送门: https://github.com/fashare2015/LinkedScrollDemo
知识点
做之前先罗列一下知识点,或者说我们能从这个demo里收获到什么。
面向抽象/接口编程
自定义 view
代理模式
UML类图
复习 listview && recyclerview 的细节
感觉做完以后收获最大的还是第一点,面向接口编程。事实上,完成功能的时间只占了一半,后边的时间一直在抽象和重构;哎,一步到位太难了,还是老老实实写具体类,再抽取基类把。
构思
UI部分
LinkedLayout
要做的呢是两个相互关联的列表,在左边的作为tab页,右边的作为content页。先不考虑交互,我们来打个界面:搞一个叫做LinkedLayout的类,用来盛放tab和content:
public class LinkedLayout extends LinearLayout {private Context mContext;private BaseScrollableContainer mTabContainer;private BaseScrollableContainer mContentContainer;private SectionIndexer mSectionIndexer; // 代理...}
public abstract class BaseScrollableContainer<VG extends ViewGroup> {protected Context mContext;public VG mViewGroup;protected RealOnScrollListener mRealOnScrollListener;private EventDispatcher mEventDispatcher;...}和我们预想的差不多嘛,mContext上下文,mViewGroup基本就是指代我们的两个listview了吧。当然,我之后可是要做toolbar+viewpager的,肯定得依赖抽象,不能直接写listview啦。余下两个是Listener,等我们界面搭好,写交互的时候在看把。
<?xml version="1.0" encoding="utf-8"?><RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"><com.fashare.linkedscrolldemo.ui.LinkedLayoutandroid:id="@+id/linked_layout"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="horizontal"/></RelativeLayout>擦,就没了嘛?剩下的得靠Java代码来搞啦。回到LinkedLayout咱们来布局UI~:
public class LinkedLayout extends LinearLayout {...private static final int MEASURE_BY_WEIGHT = 0;private static final float WEIGHT_TAB = 1;private static final float WEIGHT_CONTENT = 3;public void setContainers(BaseScrollableContainer tabContainer, BaseScrollableContainer contentContainer) {mTabContainer = tabContainer;mContentContainer = contentContainer;mTabContainer.setEventDispatcher(this);mContentContainer.setEventDispatcher(this);// 设置 LayoutParamsmTabContainer.mViewGroup.setLayoutParams(new LinearLayout.LayoutParams(MEASURE_BY_WEIGHT,ViewGroup.LayoutParams.WRAP_CONTENT,WEIGHT_TAB));mContentContainer.mViewGroup.setLayoutParams(new LinearLayout.LayoutParams(MEASURE_BY_WEIGHT,ViewGroup.LayoutParams.MATCH_PARENT,WEIGHT_CONTENT));this.addView(mTabContainer.mViewGroup);this.addView(mContentContainer.mViewGroup);this.setOrientation(HORIZONTAL);}}搞了个setContainers用来注入我们的Container,里边有一些像layout_height,layout_width,layout_weight,orientation之类的,很眼熟吧,和xml没差。顺便一提的是,我们用了weight属性来控制这个比例1:3,一直感觉这个属性比较神奇。。。
mTabContainer = new ListViewTabContainer(this, mListView); mContentContainer = new RecyclerViewContentContainer(this, mRecyclerView);看名字一个是listview填充的tab,一个是recyclerview填充的content。就先实现这两个类吧,从图中可以看到,它们分别继承于BaseScrollableContainer,并被LinkedLayout所持有:
交互部分
与用户的交互:OnScrollListener 与 代理模式
终于到了交互部分,既然是滑动,那少不了定义监听器啦。然而,麻烦在于listview和recyclerview各自的OnScrollListener还不一样,这个时候如果各自实现的话,既麻烦,又有冗余。像这样子:
// RecyclerViewpublic class RecyclerViewContentContainer extends BaseScrollableContainer<RecyclerView> {...@Overrideprotected void setOnScrollListener() {mViewGroup.addOnScrollListener(new ProxyOnScrollListener());}private class ProxyOnScrollListener extends RecyclerView.OnScrollListener {@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {if(newState == RecyclerView.SCROLL_STATE_IDLE) {// 停止滑动1.停止时的逻辑...}else if(newState == RecyclerView.SCROLL_STATE_DRAGGING){// 按下拖动2.刚刚拖动时的逻辑...}}@Overridepublic void onScrolled(RecyclerView recyclerView, int dx, int dy) { // 滑动3.滑动时的逻辑...}}}// ListViewpublic class ListViewTabContainer extends BaseScrollableContainer<ListView> {...@Overrideprotected void setOnScrollListener() {mViewGroup.setOnScrollListener(new ProxyOnScrollListener());...}public class ProxyOnScrollListener implements AbsListView.OnScrollListener{@Overridepublic void onScrollStateChanged(AbsListView view, int scrollState) {if(scrollState == SCROLL_STATE_IDLE) { // 停止滑动1.停止时的逻辑...}else if(scrollState == SCROLL_STATE_TOUCH_SCROLL) // 按下拖动2.刚刚拖动时的逻辑...}@Overridepublic void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {3.滑动时的逻辑...// 滑动}}}那该怎么办呢,虽然各自的OnScrollListener差异挺大,但是仔细观察可以发现其实很多逻辑都是类似的,可以共用的。这时恰恰可以用代理模式来做重构。我抽取了1、2、3处的逻辑,由于在抽象意义上是一致的,可以整理成接口:
public interface OnScrollListener {// tab 点击事件void onClick(int position);// 1.滑动开始void onScrollStart();// 2.滑动结束void onScrollStop();// 3.触发 onScrolled()void onScrolled();// 用户手动滑, 触发的 onScrolled()void onScrolledByUser();// 程序调用 scrollTo(), 触发的 onScrolled()void onScrolledByInvoked();}与此同时,RecyclerView和ListView各自的监听器便分别作为代理类,把1、2、3的逻辑都委托给某个接盘侠,不必自己去实现,倒也落的轻松自在。如图所示:这里写图片描述
public class RealOnScrollListener implements OnScrollListener {public boolean isTouching = false; // 处于触摸状态private int mCurPosition = 0;// 当前选中项private BaseViewGroupUtil<VG> mViewUtil; // ViewGroup 工具类...}isTouching:
两个Container之间的交互
之前都是对用户的交互,终于到联动部分了。不急着实现,先回答我一个问题:假设我一个Activity里持有两个Fragment,问它们之间如何通信?
A同学大声道:用广播
B同学:EventBus !!!
C同学:看我 RxBus 。。。
别闹好吗。。。给我老老实实用Listener。显然,我们这里面临的是同样的场景。LinkedLayout=Activity,Container=Fragment。
动手前先定义Listener吧,要取个中二点的名字:
/* * 事件分发者 */public interface EventDispatcher {/** * 分发事件: fromView 中的 pos 被选中 * @param pos * @param fromView */void dispatchItemSelectedEvent(int pos, View fromView);}/* * 事件接受者 */public interface EventReceiver {/** * 收到事件: 立即选中 newPos * @param newPos */void selectItem(int newPos);}然后LinkedLayout作为父级元素,肯定是分发者的角色,应当实现EventDispatcher;而BaseScrollableContainer作为子元素,接受该事件,应当实现EventReceiver。看下类图:
看下相应的实现(EventReceiver):
public abstract class BaseScrollableContainer<VG extends ViewGroup>implements EventReceiver {protected RealOnScrollListener mRealOnScrollListener;private EventDispatcher mEventDispatcher; // 持有分发者...public void setEventDispatcher(EventDispatcher eventDispatcher) {mEventDispatcher = eventDispatcher;}// 掉用 mEventDispatcher,也就是 LinkedLayoutprotected void dispatchItemSelectedEvent(int curPosition){if(mEventDispatcher != null)mEventDispatcher.dispatchItemSelectedEvent(curPosition, mViewGroup);}@Overridepublic void selectItem(int newPos) {mRealOnScrollListener.selectItem(newPos);}// OnScrollListener: 代理模式public class RealOnScrollListener implements OnScrollListener {...public void selectItem(int position){mCurPosition = position;Log.d("setitem", position + "");// 来自另一边的联动事件mViewUtil.smoothScrollTo(position);//if(mViewUtil.isVisiblePos(position))// curSection 可见时, 不滚动mViewUtil.setViewSelected(position);}@Overridepublic void onClick(int position) {isTouching = true;mViewUtil.setViewSelected(mCurPosition = position);dispatchItemSelectedEvent(position); // 点击tab,分发事件isTouching = false;}...@Overridepublic void onScrolled() {mCurPosition = mViewUtil.updatePosOnScrolled(mCurPosition);if(isTouching) // 来自用户, 通知 对方 联动onScrolledByUser();else// 来自对方, 被动滑动不响应onScrolledByInvoked();}@Overridepublic void onScrolledByUser() {dispatchItemSelectedEvent(mCurPosition);// 来自用户, 通知 对方 联动}}}再看(EventDispatcher):
public class LinkedLayout extends LinearLayout implements EventDispatcher {private BaseScrollableContainer mTabContainer;private BaseScrollableContainer mContentContainer;private SectionIndexer mSectionIndexer; // 分组接口...@Overridepublic void dispatchItemSelectedEvent(int pos, View fromView) {if (fromView == mContentContainer.mViewGroup) { // 来自 content, 转发给 tabint convertPos = mSectionIndexer.getSectionForPosition(pos);mTabContainer.selectItem(convertPos);} else {// 来自 tab, 转发给 contentint convertPos = mSectionIndexer.getPositionForSection(pos);mContentContainer.selectItem(convertPos);}}}总结