Android模仿实现闲鱼首页的思路与方法

首先我们来看看效果图

Demo是基于MVVM模式来编写的,欢迎大家给予批评和指正。

其中Banner的无限轮播用了PageSnapHelper,后续RecycleView也可以实现更多类似ViewPage的效果了

可以看出页面大概可以分为这几个部分

1.最上面是一个轮播的Banner

2.中间可能有些其他的功能列表

3.最后是Tab页(这里是新鲜的和附近的两个列表)

OK,看到这样的布局需求的时候可能有两种思路

1、整体是一个RefreshLayout布局,内嵌RecycleView,而Banner页,其他功能列表以及TabLayout都当成RecycleView的头加入到RecycleView中,TabLayout下面是真正的列表项

2、整体还是一个RefreshLayout布局,内部是一个NestScrollView,Banner页,其他功能列表,TabLayout依次布局在NestScrollView中,然后最下面布局一个FrameLayout,TabLayout切换的时候切换不同的Fragment

Demo中使用的是第一种方式,第二种方式考虑到SwipeRefreshLayout和内部FrameLayout的滑动会有冲突,后续再尝试编写

接下来考虑需要考虑的问题

  1. TabLayout需要固定到顶部
  2. 第一次加载数据的时候需要有个Loading提示,Demo中就是一个小鱼的空白等待页
  3. 因为使用一个数据集,在TabLayout来回切换的时候需要保证数据集合所在的位置是正确的(比如新鲜的这个列表当前在Position1的位置,切换到附近的列表我滑到了Position2的位置,当我再切回新鲜的时候需要回到Position1的位置)

下面就一些核心的代码和思路讲解一下

首先是布局,布局很简单,SwipeRefreshLayout中包了一个FrameLayout,然后在FrameLayout中包含了一个RecycleView

<android.support.v4.widget.SwipeRefreshLayout
 android:id="@+id/layout_refresh"
 android:layout_width="match_parent"
 android:layout_height="match_parent">

 <FrameLayout
  android:id="@+id/container"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <android.support.v7.widget.RecyclerView
  android:id="@+id/list"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />
 </FrameLayout>
 </android.support.v4.widget.SwipeRefreshLayout>

接下来看下StickyHead如何实现

//正常的TabLayout布局
private TabLayout mTabLayout;
//粘性 TabLayout布局(用于固定在顶部)
private TabLayout mStickyTabLayout;
//粘性布局的Y坐标(用户判断粘性布局是否显示)
private int mStickyPositionY;
//主列表布局
private RecyclerView mHomeList;

这是变量的定义,下面的这个类是我将一些页面逻辑涉及的变量抽离出来

public class HomeEntity extends BaseObservable {

//列表类型 0:新鲜的 1:附近的
public static final int LIST_TYPE_FRESH = 0;
public static final int LIST_TYPE_NEAR = 1;

private int bannerCount;
private int listType = LIST_TYPE_FRESH;
//新鲜的和附近的首次加载的loading状态
private boolean refreshLoading;
private boolean nearLoading;
//首页是否正在下拉刷新
private boolean refreshing;
//新鲜的和附近的 获取更多的View的状态值(用户记录TabLayout切换的时候,LoadingMore的状态)
private int refreshMoreStatus;
private int nearMoreStatus;
//首页的活动更多的状态
private int loadingMoreStatus;
}

这是变量的定义,然后初始化两个TabLayout,主要在于需要监听TabLayout的切换

mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
 @Override
 public void onTabSelected(TabLayout.Tab tab) {
  int position = tab.getPosition();
  //设置粘贴TabLayout的选中Tab
  if (!mStickyTabLayout.getTabAt(position).isSelected()) {
  mStickyTabLayout.getTabAt(position).select();
  mViewModel.changeHomeData(position);
  }
 }

 @Override
 public void onTabUnselected(TabLayout.Tab tab) {

 }

 @Override
 public void onTabReselected(TabLayout.Tab tab) {

 }
 });
mStickyTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
 @Override
 public void onTabSelected(TabLayout.Tab tab) {
  int position = tab.getPosition();
  if (!mTabLayout.getTabAt(position).isSelected()) {
  mTabLayout.getTabAt(position).select();
  mHomeList.stopScroll();

  //mAdapter.setEnableLoadMore(false);
  mViewModel.changeHomeData(position);
  ......
  }
 }

 @Override
 public void onTabUnselected(TabLayout.Tab tab) {

 }

 @Override
 public void onTabReselected(TabLayout.Tab tab) {

 }
 });

这段的逻辑比较简单,就是实现了保持TabLayout切换状态的统一,当TabLayout切换的时候,需要将StickyTabLayout所选中的Tab也设置一下,mViewModel.changeHomeData(position)这句话是为了切换数据,下面会分析到

接下来是StickyHead重要的代码

mHomeList.addOnScrollListener(new RecyclerView.OnScrollListener() {
 @Override
 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
  int[] location = new int[2];
  mTabLayout.getLocationInWindow(location);
  int count = mViewContainer.getChildCount();
  if (location[1] <= mStickyPositionY) {
  if (count == 1) {
   mViewContainer.addView(mStickyTabLayout);
   mBinding.layoutRefresh.setEnabled(false);
  }
  } else {
  if (count > 1) {
   mViewContainer.removeView(mStickyTabLayout);
   //mOffsetY = DisplayUtil.dip2px(mContainer.getContext(), 46);
   //mRefreshPosition = mAdapter.getHeaderLayoutCount();
   //mNearPosition = mAdapter.getHeaderLayoutCount();
   mBinding.layoutRefresh.setEnabled(true);
  }
  }

  //if (mInitPositionY == -1) {
  //mInitPositionY = location[1];
  //}
  //mHomeListPositionY = location[1];
 }
 });

主要逻辑就是先获取TabLayout在窗口的位置,如果Y坐标小于粘贴头部的Y坐标,则将粘贴头部加入到布局中来并显示,否则,将粘贴头部布局从布局中移除。判断count这个值是为了防止重复添加和重复移除粘贴头布局。

mBinding.layoutRefresh.setEnabled(true/false)是为了在粘贴头部固定在顶上的时候消除掉外层SwipeRefreshLayout的下拉刷新错误。注释掉的代码会在下面再讲

只需要上面的这么多代码一个StickyHead就实现了,在测试的时候遇到点小问题,就是焦点重置导致的RecycleView重新回到初始位置的一个错误,下面是暂时的解决方案

LinearLayoutManager manager = new LinearLayoutManager(mContainer.getContext()) {
 @Override
 public boolean onRequestChildFocus(RecyclerView parent, RecyclerView.State state, View child, View focused) {
  //TODO 暂时处理View焦点问题
  return true;
 }
 };

下面简单说下如何实现首次加载新鲜的或者附近的数据的时候出现的一个等待页面

主要思路是这样的

1、这个等待的LoadingView是当成RecycleView的头加在TabLayout后面的,当数据加载完成这个LoadingView设置为不可见

2、因为有TabLayout会切换,导致RecycleView的数据会重新绘制,进而导致RecyView会回到初始位置,所以需要记录下RecycleView所在的位置,然后手动滑动到记录的位置

具体的我们还是来看代码吧

private int mHomeListPositionY;//用来标识当前RecycleView的位置
private int mInitPositionY = -1;//初始状态下RecycleView的Y坐标
//这里是RecycleView的滑动监听,用来记录RecycleView的位置,这里其实是记录了mTabLayout的位置
mHomeList.addOnScrollListener(new RecyclerView.OnScrollListener() {
 @Override
 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
  int[] location = new int[2];
  mTabLayout.getLocationInWindow(location);
  int count = mViewContainer.getChildCount();

  if (mInitPositionY == -1) {
  mInitPositionY = location[1];
  }
  mHomeListPositionY = location[1];
 }
 });
//这个函数就是用来手动将RecycleView滑动到正确的位置
 private void setLoadingView(boolean visible, int type) {
 int position;
 if (type == HomeEntity.LIST_TYPE_FRESH) {
 position = mRefreshPosition;
 } else {
 position = mNearPosition;
 }
 if (visible) {
 mLoadingView.setVisibility(View.VISIBLE);
 if (mStickyTabLayout.getVisibility() == View.VISIBLE) {
  LinearLayoutManager layoutManager = (LinearLayoutManager) mHomeList.getLayoutManager();
  layoutManager.scrollToPositionWithOffset(0, mHomeListPositionY - mInitPositionY);
 }
 } else {
 mLoadingView.setVisibility(View.GONE);
 LinearLayoutManager layoutManager = (LinearLayoutManager) mHomeList.getLayoutManager();
 if (mViewContainer.getChildCount() > 1) {
  layoutManager.scrollToPositionWithOffset(position, mStickyTabLayout.getHeight());
 } else {
  layoutManager.scrollToPositionWithOffset(0, mHomeListPositionY - mInitPositionY);
 }
 }
}

最后来看下新鲜的和附近的加载更多时页面的实现

这里Adapter使用了第三方BRVAH,所以相对LoadingMore的状态BRVAH帮我封了一下,因为虽然是一个List,但其实是两个列表复用一个List的,所以这里的LoadingMore状态我们需要记录两个,方便切换的时候列表的LoadingMore状态是正确的,下面看下主要代码

if (propertyId == BR.refreshLoading) {
  if (HomeEntity.LIST_TYPE_FRESH != entity.getListType()) {
  return;
  }
  if (mLoadingView.getVisibility() == View.GONE) {
  mAdapter.setEnableLoadMore(true);
  }
 } else if (propertyId == BR.nearLoading) {
  if (HomeEntity.LIST_TYPE_NEAR != entity.getListType()) {
  return;
  }
  if (mLoadingView.getVisibility() == View.GONE) {
  mAdapter.setEnableLoadMore(true);
  }
 } else if (propertyId == BR.loadingMoreStatus) {
  int status = entity.getLoadingMoreStatus();
  mAdapter.setEnableLoadMore(true);
  if (LoadMoreView.STATUS_DEFAULT == status) {
  mAdapter.loadMoreComplete();
  } else if (LoadMoreView.STATUS_END == status) {
  mAdapter.loadMoreEnd();
  } else if (LoadMoreView.STATUS_FAIL == status) {
  mAdapter.loadMoreFail();
  }
 }

BR.refreshLoadingBR.nearLoading 都是监听首次加载,这里

if (mLoadingView.getVisibility() == View.GONE) {
mAdapter.setEnableLoadMore(true);
}

这个是为了防止首次加载显示Loading页面的时候又显示了LoadingMore布局

BR.loadingMoreStatus这个就是监听LoadingMore的状态来更新List的Adapter

其他的主要ViewModel代码在HomeViewModel中。

主要的几个点

  • 粘贴头布局的逻辑
  • TabLayout切换导致数据集变化以及位置的变化
  • 加载更多的时候需要考虑TabLayout切换的问题

项目链接:https://github.com/ly85206559/demo4Fish

本地下载:点击这里

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Android模仿实现闲鱼首页的思路与方法

    首先我们来看看效果图 Demo是基于MVVM模式来编写的,欢迎大家给予批评和指正. 其中Banner的无限轮播用了PageSnapHelper,后续RecycleView也可以实现更多类似ViewPage的效果了 可以看出页面大概可以分为这几个部分 1.最上面是一个轮播的Banner 2.中间可能有些其他的功能列表 3.最后是Tab页(这里是新鲜的和附近的两个列表) OK,看到这样的布局需求的时候可能有两种思路 1.整体是一个RefreshLayout布局,内嵌RecycleView,而Bann

  • Android模仿To圈儿个人资料界面层叠淡入淡出显示效果

    前几天做的一个仿To圈个人资料界面的实现效果 下面是To圈的效果Gif图: 做这个东西其实也花了一下午的时间,一开始思路一直没理清楚,就开始盲目的去做,结果反而事倍功半. 以后要吸取教训,先详细思考清楚其中的逻辑关系,然后再开始动手写代码,这样比较容易理顺. 可以看到实现这个效果还是不难的,得分成以下三个步骤: 1:首先要有一个可拖动的详细资料布局(下半部分). 2:上半部分可跟随移动. 3:标题栏由隐藏到显示. 涉及到的技术点有: 1:屏幕像素密度DP转化. 2:自定义视图的OnTouchLi

  • Android模仿实现微博详情页滑动固定顶部栏的效果实例

    前言 最近项目中遇到一个需求,类似微博详情页的效果,通过查找相关的资料终于找了对应的解决方案,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 先来看下我们今天要实现的效果: 滑动固定顶部栏效果图 这段时间公司准备重构一个项目,刚好用到这个效果,我就顺带写了篇文章,关于这个效果网上可以找到一些相关资料的,昨晚看了一些,感觉都不是很好,有点模棱两可的样子,也没提到需要注意的一些关键点,这里来做下整理,由于涉及到公司的代码,这里我就写个简单的Demo来讲解. 简单Demo 传统套路:

  • Android模仿美团顶部的滑动菜单实例代码

    前言 本文主要给大家介绍了关于Android模仿美团顶部滑动菜单的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 先来看下效果图: 实现方法 这是通过 ViewPager 和 GridView 相结合做出来的效果,每一个 ViewPager 页面都是一个 GridView,底部的每个滑动指示圆点都是从布局文件中 inflate 出来的 首先需要一个代表每个活动主题的 JavaBean /** * Created by CZY on 2017/6/23. */ publ

  • Android模仿用户设置密码实例

    首先有2个对话框,没有设置过密码,需要设置dialog_set_password.xml,用户设置过密码,不需要设置,直接输入密码dialog_input_password.xml, 设置对话框dialog_set_password.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/r

  • Android模仿微信收藏文件的标签处理功能

    最近需要用到微信的标签功能(如下图所示).该功能可以添加已有标签,也可以自定义标签.也可以删除已编辑菜单.研究了一番.发现还是挺有意思的,模拟实现相关功能. 该功能使用类似FlowLayout的功能.Flowlayout为一个开源软件(https://github.com/ApmeM/android-flowlayout),功能为自动换行的布局类型 import android.content.Context; import android.util.AttributeSet; import a

  • Android实现换肤的两种思路分析

    本文分析了Android实现换肤的两种思路.分享给大家供大家参考,具体如下: 这里来了解换肤实现及不同方案的差异和使用场合. 一.从功能上划分 1) 软件内置多个皮肤,用户不能修改: 2) 官方提供皮肤下载,用户使用下载的皮肤: 3) 官方提供皮肤制作工具或方法,用户自制皮肤. 二.皮肤定义 软件皮肤包括图标.字体.布局.交互风格等,换肤就是换掉皮肤包括的部分或所有资源. 三.皮肤与APP分离 1)打包皮肤文件 默认格式是apk.例如Launcher,它的桌面皮肤格式是一个apk: 自定义的格式

  • Android 模仿QQ侧滑删除ListView功能示例

    需求: 1.listView可以侧滑item,展示删除按钮,点击删除按钮,删除当前的item 2.在删除按钮展示时,点击隐藏删除按钮,不响应item的点击事件 3.在删除按钮隐藏时,点击item响应点击事件 根据以上需求在网络上查找响应的例子,也有仿QQ侧滑代码,但不能满足2和3的要求,因此修改了一把,代码如下,共大家拍砖 第一步:重写ListView public class SwipeListView extends ListView { private final static Strin

  • python爬虫利用selenium实现自动翻页爬取某鱼数据的思路详解

    基本思路: 首先用开发者工具找到需要提取数据的标签列 利用xpath定位需要提取数据的列表 然后再逐个提取相应的数据: 保存数据到csv: 利用开发者工具找到下一页按钮所在标签: 利用xpath提取此标签对象并返回: 调用点击事件,并循环上述过程: 最终效果图: 代码: from selenium import webdriver import time import re class Douyu(object): def __init__(self): # 开始时的url self.start

  • Android获取和读取短信验证码的实现方法

    现如今,验证码在Android的客户端还是非常普遍的.通过手机账号和验证码直接去注册应用账户的信息.很多应用都以这种方式来完成注册.简单的介绍一下吧. Android获取短信验证码还是比较简单的,通过Mob官网提供的ShareSDK,调用其中内部的方法,就可以获取到短信的验证码了.提供一下Mob的官网地址.http://www.mob.com/#/在官网上注册相关的信息之后,下载相关的jar包和.so文件就可以实现获取短信验证码了(2.0之前的版本都需要下载jar包和 .so文件,而现在的2.2

随机推荐