云南移动12530,采购投标书,老师您辛苦了英文
本周末外卖点得多,就仿一仿“饿了么”好了。先上图吧,这样的订单页面是不是很眼熟:
右边的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; // 代理 ... }
我们让它继承了linearlayout,同时持有两个container的东东,还有一个上帝对象mcontext,以及一个分组用的sectionindexer。
basescrollablecontainer
先别管这些,主要看两个container,从名字上看一个是tab页,一个是content页,嘿嘿。因为它们都能scroll嘛,干脆搞一个basescrollablecontainer把。取名为container呢,当然是致敬fragment啦。我们来定义一下这个类:
初步一想,无非有一个 mcontext, 一个 viewgroup, 还有一些 listener 嘛:
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,等我们界面搭好,写交互的时候在看把。
看来uml图还是有好处的,继承和依赖关系一目了然。
自定义view && 动态布局
好了到了自定义view地环节了。我们已经有了一个linkedlayout,这是我们的activity_main.xml布局代码:
<?xml version="1.0" encoding="utf-8"?> <relativelayout xmlns: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.linkedlayout android: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); // 设置 layoutparams mtabcontainer.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,一直感觉这个属性比较神奇。。。
注入viewgroup, 使用自定义的linkedlayout
到这里为止,linkedlayout已经布局好了,我们分别注入viewgroup就可以用了。我这里分别用listview作tab,recyclerview作content。想像力有限,用来用去好像也就这么几个控件。。。这部分代码很简单,在mainactivity里,就不贴了。
子类化 basescrollablecontainer
按照常理,下边应该实现基类了吧。前面的mainactivity中,我们是这样实例化的:
mtabcontainer = new listviewtabcontainer(this, mlistview); mcontentcontainer = new recyclerviewcontentcontainer(this, mrecyclerview);
看名字一个是listview填充的tab,一个是recyclerview填充的content。就先实现这两个类吧,从图中可以看到,它们分别继承于basescrollablecontainer,并被linkedlayout所持有:
交互部分
与用户的交互:onscrolllistener 与 代理模式
终于到了交互部分,既然是滑动,那少不了定义监听器啦。然而,麻烦在于listview和recyclerview各自的onscrolllistener还不一样,这个时候如果各自实现的话,既麻烦,又有冗余。像这样子:
// recyclerview public class recyclerviewcontentcontainer extends basescrollablecontainer<recyclerview> { ... @override protected void setonscrolllistener() { mviewgroup.addonscrolllistener(new proxyonscrolllistener()); } private class proxyonscrolllistener extends recyclerview.onscrolllistener { @override public void onscrollstatechanged(recyclerview recyclerview, int newstate) { if(newstate == recyclerview.scroll_state_idle) { // 停止滑动 1.停止时的逻辑... }else if(newstate == recyclerview.scroll_state_dragging){ // 按下拖动 2.刚刚拖动时的逻辑... } } @override public void onscrolled(recyclerview recyclerview, int dx, int dy) { // 滑动 3.滑动时的逻辑... } } } // listview public class listviewtabcontainer extends basescrollablecontainer<listview> { ... @override protected void setonscrolllistener() { mviewgroup.setonscrolllistener(new proxyonscrolllistener()); ... } public class proxyonscrolllistener implements abslistview.onscrolllistener{ @override public void onscrollstatechanged(abslistview view, int scrollstate) { if(scrollstate == scroll_state_idle) { // 停止滑动 1.停止时的逻辑... }else if(scrollstate == scroll_state_touch_scroll) // 按下拖动 2.刚刚拖动时的逻辑... } @override public 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的逻辑都委托给某个接盘侠,不必自己去实现,倒也落的轻松自在。如图所示:这里写图片描述
然后,让我们来看看这个接盘侠:realonscrolllistener。。。
不愧是一个老实类,它老实地接盘了onscrolllistener的所有接口,并被两个代理类proxy…所持有(图中并未画出。。)。
具体实现就不贴了,大家可以下源码来看。这里大致分析一下,它有三个成员:
public class realonscrolllistener implements onscrolllistener { public boolean istouching = false; // 处于触摸状态 private int mcurposition = 0; // 当前选中项 private baseviewgrouputil<vg> mviewutil; // viewgroup 工具类 ... }
istouching:
为啥要维护这个触摸状态呢?这是由于我们的效果是联动的。这就比较讨厌了,当onscrolled()被调用,我们分不清是用户的滑动,还是来自另一个列表滑动时的联动效果。那我们记录一下istouching状态呢,就能区分开这两种情况了。
更改istouching的逻辑在onscrollstart()和onscrollstop()里边。
mcurposition:
这个很好解释,我们每次滑动需要记录当前位置,然后通知另一个列表进行联动。
这段逻辑在onscrolled()里边。
mviewutil:
一个工具库,用于简化逻辑。大概有scrollto(),setviewselected(),updateposonscrolled()等方法,如图:
两个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,也就是 linkedlayout protected void dispatchitemselectedevent(int curposition){ if(meventdispatcher != null) meventdispatcher.dispatchitemselectedevent(curposition, mviewgroup); } @override public 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); } @override public void onclick(int position) { istouching = true; mviewutil.setviewselected(mcurposition = position); dispatchitemselectedevent(position); // 点击tab,分发事件 istouching = false; } ... @override public void onscrolled() { mcurposition = mviewutil.updateposonscrolled(mcurposition); if(istouching) // 来自用户, 通知 对方 联动 onscrolledbyuser(); else // 来自对方, 被动滑动不响应 onscrolledbyinvoked(); } @override public void onscrolledbyuser() { dispatchitemselectedevent(mcurposition); // 来自用户, 通知 对方 联动 } } }
再看(eventdispatcher):
public class linkedlayout extends linearlayout implements eventdispatcher { private basescrollablecontainer mtabcontainer; private basescrollablecontainer mcontentcontainer; private sectionindexer msectionindexer; // 分组接口 ... @override public void dispatchitemselectedevent(int pos, view fromview) { if (fromview == mcontentcontainer.mviewgroup) { // 来自 content, 转发给 tab int convertpos = msectionindexer.getsectionforposition(pos); mtabcontainer.selectitem(convertpos); } else { // 来自 tab, 转发给 content int convertpos = msectionindexer.getpositionforsection(pos); mcontentcontainer.selectitem(convertpos); } } }
总结
到此为止,有没有一种酣畅淋漓的感觉?不管怎么说,面向对象是信仰,定义好接口以后,实现起来怎么写怎么舒服。
// todo: 之前说了,这个联动是通用的。之后有时间会继续实现一个toolbar+viewpager的联动…
彩蛋
高清无码类图:(完整)
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
Android studio 解决logcat无过滤工具栏的操作
Android Studio 恢复小窗口停靠模式(Docked Mode)
Android studio保存logcat日志到本地的操作
Android Studio快捷键生成TAG、Log.x日志输出介绍
网友评论