Android仿eleme点餐页面二级联动列表

家电修理 2023-07-16 19:17www.caominkang.com电器维修

本周末外卖点得多,就仿一仿“饿了么”好了。先上图吧,这样的订单页面是不是很眼熟

右边的listvie分好组以后,在左边的Tab页建立索引。可以直接导航,是不是很方便。关键在于右边滑动,左边也会跟着滑;而点击左边呢,也能定位右边的项。它们存在这样一种特殊的交互。像这种联动的效果,还有些常见的例子呢,比如知乎采用了常见的toolbar+viePager的联动,只不过是上下布局

再看看点评,它的城市选择页面也有这种联动的影子,只是稍微弱一点。侧边栏可以对listvie进行索引,这最早是在微信好友列表里出现的把

趁着周末,我也撸一个。就拓展性而言,应该可以适配以上所有情况吧。我称其为linkedLayout,看下效果图

我把右边按5个一组,可以看到,左边的索引 = 右边/5

特点

右边滑动,左边跟着动

左边滑动到边界,右边跟着动

点击左边tab项,右边滑动定位到相应的group

源码

github 传送门: https://github./fashare2015/linkedScrollDemo

知识点

做之前先罗列一下知识点,或者说我们能从这个demo里收获到什么。

面向抽象/接口编程

自定义 vie

代理模式

UML类图

复习 listvie && recyclervie 的细节

感觉做完以后收获最大的还是第一点,面向接口编程。事实上,完成功能的时间只占了一半,后边的时间一直在抽象和重构;哎,一步到位太难了,还是老老实实写具体类,再抽取基类把。

构思

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, 一个 vieGroup, 还有一些 Listener 嘛

public abstract class baseScrollableContainer {
  protected Context mContext;
  public VG mVieGroup;
  protected RealonScrollListener mRealOnScrollListener;
  private EventDispatcher mEventDispatcher;
  ...
}

和我们预想的差不多嘛,mContext上下文,mVieGroup基本就是指代我们的两个listvie了吧。,我之后可是要做toolbar+viepager的,肯定得依赖抽象,不能直接写listvie啦。余下两个是Listener,等我们界面搭好,写交互的时候在看把。

看来UML图还是有好处的,继承和依赖关系一目了然。

自定义Vie && 动态布局

好了到了自定义vie地环节了。我们已经有了一个linkedLayout,这是我们的activity_main.xml布局代码:




  <.fashare.linkedscrolldemo.ui.linkedLayout
 android:id="@+id/linked_layout"
 android:layout_idth="match_parent"
 android:layout_height="match_parent"
 android:orientation="horizontal"/>

擦,就没了嘛?剩下的得靠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.mVieGroup.setLayoutParams(ne LinearLayout.LayoutParams(
 MEASURE_BY_WEIGHT,
 VieGroup.LayoutParams.WRAP_CONTENT,
 WEIGHT_TAB
 ));

 mContentContainer.mVieGroup.setLayoutParams(ne LinearLayout.LayoutParams(
 MEASURE_BY_WEIGHT,
 VieGroup.LayoutParams.MATCH_PARENT,
 WEIGHT_ConTENT
 ));

 this.addVie(mTabContainer.mVieGroup);
 this.addVie(mContentContainer.mVieGroup);
 this.setOrientation(HORIZONTAL);
  }
}

搞了个setContainers用来注入我们的Container,里边有一些像layout_height,layout_idth,layout_eight,orientation之类的,很眼熟吧,和xml没差。顺便一提的是,我们用了eight属性来控制这个比例1:3,一直感觉这个属性比较神奇。。。

注入VieGroup, 使用自定义的linkedLayout

到这里为止,linkedLayout已经布局好了,我们分别注入VieGroup就可以用了。我这里分别用listvie作tab,recyclervie作content。想像力有限,用来用去好像也就这么几个控件。。。这部分代码很简单,在MainActivity里,就不贴了。

子类化 baseScrollableContainer

按照常理,下边应该实现基类了吧。前面的MainActivity中,我们是这样实例化的

mTabContainer = ne ListVieTabContainer(this, mListVie); 
mContentContainer = ne RecyclerVieContentContainer(this, mRecyclerVie);

看名字一个是listvie填充的tab,一个是recyclervie填充的content。就先实现这两个类吧,从图中可以看到,它们分别继承于baseScrollableContainer,并被linkedLayout所持有:

 

交互部分

与用户的交互onScrollListener 与 代理模式

终于到了交互部分,既然是滑动,那少不了定义监听器啦。,麻烦在于listvie和recyclervie各自的OnScrollListener还不一样,这个时候如果各自实现的话,既麻烦,又有冗余。像这样子

// RecyclerVie
public class RecyclerVieContentContainer extends baseScrollableContainer {
  ...
  @Override
  protected void setonScrollListener() {
 mVieGroup.addonScrollListener(ne ProxyonScrollListener());
  }

  private class ProxyonScrollListener extends RecyclerVie.onScrollListener {
 @Override
 public void onScrollStateChanged(RecyclerVie recyclerVie, int neState) {
   if(neState == RecyclerVie.SCROLL_STATE_IDLE) {   // 停止滑动
 1.停止时的逻辑...
   }else if(neState == RecyclerVie.SCROLL_STATE_DRAGGING){  // 按下拖动
 2.刚刚拖动时的逻辑...
   }
 }

 @Override
 public void onScrolled(RecyclerVie recyclerVie, int dx, int dy) { // 滑动
   3.滑动时的逻辑...
 }
  }
}

// ListVie
public class ListVieTabContainer extends baseScrollableContainer {
  ...
  @Override
  protected void setonScrollListener() {
 mVieGroup.setonScrollListener(ne ProxyonScrollListener());
 ...
  }

  public class ProxyonScrollListener implements AbsListVie.OnScrollListener{
 @Override
 public void onScrollStateChanged(AbsListVie vie, int scrollState) {
   if(scrollState == SCROLL_STATE_IDLE) {// 停止滑动
 1.停止时的逻辑...
   }else if(scrollState == SCROLL_STATE_TOUCH_SCROLL) // 按下拖动
 2.刚刚拖动时的逻辑...
 }

 @Override
 public void onScroll(AbsListVie vie, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
   3.滑动时的逻辑... // 滑动
 }
  }
}

那该怎么办呢,虽然各自的OnScrollListener差异挺大,仔细观察可以发现其实很多逻辑都是类似的,可以共用的。这时恰恰可以用代理模式来做重构。我抽取了1、2、3处的逻辑,由于在抽象意义上是一致的,可以整理成接口

public interface onScrollListener {
  // tab 点击事件
  void onClick(int position);

  // 1.滑动开始
  void onScrollStart();

  // 2.滑动结束
  void onScrollS();

  // 3.触发 onScrolled()
  void onScrolled();

  // 用户手动滑, 触发的 onScrolled()
  void onScrolledByUser();

  // 程序调用 scrollTo(), 触发的 onScrolled()
  void onScrolledByInvoked();
}

与此,RecyclerVie和ListVie各自的监听器便分别作为代理类,把1、2、3的逻辑都委托给某个接盘侠,不必自己去实现,倒也落的轻松自在。如图所示这里写图片描述

然后,让我们来看看这个接盘侠:RealOnScrollListener。。。

不愧是一个老实类,它老实地接盘了OnScrollListener的所有接口,并被两个代理类Proxy…所持有(图中并未画出。。)。
具体实现就不贴了,大家可以下源码来看。这里大致分析一下,它有三个成员

public class RealonScrollListener implements onScrollListener {
  public boolean isTouching = false; // 处于触摸状态
  private int mCurPosition = 0; // 当前选中项
  private baseVieGroupUtil mVieUtil; // VieGroup 工具类
  ...
}

isTouching:

为啥要维护这个触摸状态呢?这是由于我们的效果是联动的。这就比较讨厌了,当onScrolled()被调用,我们分不清是用户的滑动,还是来自另一个列表滑动时的联动效果。那我们记录一下isTouching状态呢,就能区分开这两种情况了。
更改isTouching的逻辑在onScrollStart()和onScrollS()里边。

mCurPosition

这个很好解释,我们每次滑动需要记录当前位置,然后通知另一个列表进行联动。
这段逻辑在onScrolled()里边。

mVieUtil
一个工具库,用于简化逻辑。大概有scrollTo(),setVieSelected(),UpdatePosonScrolled()等方法,如图

 

两个Container之间的交互

之前都是对用户的交互,终于到联动部分了。不急着实现,先回答我一个问题假设我一个Activity里持有两个Fragment,问它们之间如何通信?

A同学大声道用广播
B同学EventBus !!!
C同学看我 RxBus 。。。
别闹好吗。。。给我老老实实用Listener。显然,我们这里面临的是同样的场景。linkedLayout=Activity,Container=Fragment。
动手前先定义Listener吧,要取个中二点的名字


public interface EventDispatcher {
  
  void dispatchItemSelectedEvent(int pos, Vie fromVie);
}

public interface EventReceiver {
  
  void selectItem(int nePos);
}

然后linkedLayout作为父级元素,肯定是分发者的角色,应当实现EventDispatcher;而baseScrollableContainer作为子元素,接受该事件,应当实现EventReceiver。看下类图

看下相应的实现(EventReceiver)

public abstract class baseScrollableContainer
 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, mVieGroup);
  }
  @Override
  public void selectItem(int nePos) {
 mRealOnScrollListener.selectItem(nePos);
  }
  // OnScrollListener: 代理模式
  public class RealonScrollListener implements onScrollListener {
 ...
 public void selectItem(int position){
   mCurPosition = position;
   Log.d("setitem", position + "");
   // 来自另一边的联动事件
   mVieUtil.smoothScrollTo(position);
//   if(mVieUtil.isVisiblePos(position))  // curSection 可见时, 不滚动
 mVieUtil.setVieSelected(position);
 }
 @Override
 public void onClick(int position) {
   isTouching = true;
   mVieUtil.setVieSelected(mCurPosition = position);
   dispatchItemSelectedEvent(position); // 点击tab,分发事件
   isTouching = false;
 }
 ...
 @Override
 public void onScrolled() {
   mCurPosition = mVieUtil.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, Vie fromVie) {
 if (fromVie == mContentContainer.mVieGroup) { // 来自 content, 转发给 tab
   int convertPos = mSectionIndexer.getSectionForPosition(pos);
   mTabContainer.selectItem(convertPos);
 } else {   // 来自 tab, 转发给 content
   int convertPos = mSectionIndexer.getPositionForSection(pos);
   mContentContainer.selectItem(convertPos);
 }
  }
}

到此为止,有没有一种酣畅淋漓的感觉?不管怎么说,面向对象是信仰,定义好接口以后,实现起来怎么写怎么舒服。
// TODO: 之前说了,这个联动是通用的。之后有时间会继续实现一个toolbar+viePager的联动…

彩蛋

高清无码类图(完整)

Copyright © 2016-2025 www.caominkang.com 曹敏电脑维修网 版权所有 Power by