Android企业级开源电商+直播app视图层全解析(保姆级完整版) ...

打印 上一主题 下一主题

主题 1876|帖子 1876|积分 5628

涵盖技术点:Banner+MagicIndicator+viewpager+RecyclerView+RxRefreshView(视图革新)+LayoutManager(标签列表布局定位)+自定义视图+Bitmap+WebView(H5)+WebView(视频流)+RxJava变乱流/观察者模式+悬浮窗(WindowManager+FloatFrameLayout)
首页界面


  1. private FrameLayout mVpTopContainer;//私信搜索购物车
  2. private FrameLayout mVpBannerContainer;//轮播图容器
  3. private MagicIndicator mIndicator;//标签栏
  4. private ViewPager mViewPager;//直播间item
  5. private BannerViewProxy<BannerBean>mBannerViewProxy;//轮播图
复制代码
轮播图计划

mBannerViewProxy轮播图外形计划
  1. mBanner.setClipToOutline(true); // 支持圆角裁剪
  2. mBanner.setOutlineProvider(new ViewOutlineProvider() {
  3.         @Override
  4.         public void getOutline(View view, Outline outline) {
  5.             int radius = DpUtil.dp2px(5); // 设置圆角半径为5dp
  6.             outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
  7.         }
  8.     });
复制代码
圆角裁剪计划:


  • setClipToOutline(true) 开启控件裁剪。
  • ViewOutlineProvider 自定义表面,实现圆角矩形效果,setRoundRect(0, 0, ...) 控制裁剪地区,radius 为圆角大小。
轮播图指示器

  1. private IndicatorView defaultIndicator() {
  2.     IndicatorView indicator = new IndicatorView(getActivity())
  3.             .setIndicatorColor(ResourceUtil.getColor(getActivity(),R.color.alpha_white_3f))// 普通指示器颜色
  4.             .setIndicatorSelectorColor(Color.WHITE)// 选中指示器颜色
  5.             .setIndicatorRatio(5f) //ratio,默认值是1 ,也就是说默认是圆点,根据这个值,值越大,拉伸越长,就成了矩形,小于1,就变扁了呗
  6.             .setIndicatorRadius(2f) // radius 点的大小
  7.             .setIndicatorSelectedRatio(5f)// 选中状态宽高比
  8.             .setIndicatorSelectedRadius(2f)// 选中状态圆角半径
  9.             .setIndicatorStyle(IndicatorView.IndicatorStyle.INDICATOR_BIG_CIRCLE);
  10.     return indicator;
  11. }
复制代码
轮播图下面的小圆点指示器,他把它改成扁横线了


  • 颜色

    • setIndicatorColor() 设置普通圆点颜色(未选中状态)。
    • setIndicatorSelectorColor() 设置选中圆点颜色。



  • 形状

    • setIndicatorRatio(5f) 将圆点拉伸成长条。
    • setIndicatorRadius(2f) 控制圆点大小。



  • 样式

    • setIndicatorStyle() 设置指示器样式,这里选择了 INDICATOR_BIG_CIRCLE。

配置轮播图:主动播放和时间隔断、设置指示器样式、设置适配器
  1. mImageAdapter = new ImageAdapter2(); // 创建适配器
  2. mImageAdapter.setNewData(mData); // 设置数据
  3. mBanner.setAutoPlay(true) // 开启自动播放
  4.         .setIndicator(mIndicatorView) // 绑定指示器
  5.        .setAdapter(mImageAdapter) // 设置适配器
  6.        .setAutoTurningTime(10000); // 自动轮播时间间隔为10秒
复制代码
绑定步调:

  • 创建 ImageAdapter2,并设置轮播数据。
  • setIndicator(mIndicatorView):绑定指示器控件。
  • setAdapter(mImageAdapter):将适配器与 Banner 关联。
  • setAutoPlay(true) + setAutoTurningTime(10000):设置主动播放功能和播放隔断时间。
  1. public class ImageAdapter2 extends BaseQuickAdapter<T, BaseViewHolder> {
  2.     public ImageAdapter2() {
  3.         super(R.layout.item_banner_image);
  4.     }
  5.     @Override
  6.     protected BaseViewHolder onCreateDefViewHolder(ViewGroup parent, int viewType) {
  7.         BaseViewHolder baseViewHolder = super.onCreateDefViewHolder(parent, viewType);
  8.         return baseViewHolder;
  9.     }
  10.     @Override
  11.     protected void convert(@NonNull BaseViewHolder helper, T item) {
  12.         Glide.with(mContext)
  13.                 .load(item.getImageUrl())
  14.                 .into((ImageView) helper.getView(R.id.img));
  15.         helper.getView(R.id.img).setTag(item);
  16.     }
  17. }
复制代码
当你给适配器传递一个 List 类型的数据时,适配器会主动遍历这个列表,并将每个 T(即每个列表中的元素,也就是这里的item)绑定到相应的视图上,他直接把循环绑定的那一步帮我们做了,以是会简便
每一份BannerBean就是一个图片(包含图片地址和点击图片跳转的网址)
List
initData中
  1. List<BannerBean> list=featureBean.getBanner();//获取轮播图的图列表
复制代码
这里就拿到了要轮播的图的列表
  1. initBannerView(list);
  2. private void initBannerView(List<BannerBean> beanList) {
  3.    if(!ListUtil.haveData(beanList)){
  4.        return;
  5.    }
  6.     if(mBannerViewProxy==null){
  7.        mBannerViewProxy=new BannerViewProxy<>();
  8.        mBannerViewProxy.setData(beanList);
  9.        getViewProxyChildMannger().addViewProxy(mVpBannerContainer, mBannerViewProxy, mBannerViewProxy.getDefaultTag());
  10.     }else{
  11.        mBannerViewProxy.update(beanList);
  12.     }
  13. }
复制代码
这里就是把列表放到mBannerViewProxy的适配器中
  1. public void setData(List<T> list) {
  2.     mData=list;
  3.     if(mImageAdapter!=null){
  4.        mImageAdapter.setNewData(list);//把list塞到适配器中
  5.     }
  6. }
复制代码
末了就得到了mBannerViewProxy
  1. BannerViewProxy<BannerBean>  mBannerViewProxy
复制代码
再把放到mBannerViewProxy容器mVpBannerContainer(FrameLayout类型)中来适配FrameLayout
  1. getViewProxyChildMannger().addViewProxy(mVpBannerContainer, mBannerViewProxy, mBannerViewProxy.getDefaultTag());
复制代码
就把轮播图放到首页上了
标签栏的每个标签对应不同类型的直播间列表


  1. private MagicIndicator mIndicator;//标签栏
  2. List<LiveClassBean>liveClassBean=featureBean.getLiveclass();//获取直播分类标签
  3. initIndicator(liveClassBean);//设置每个标签的对应直播间列表
复制代码
先拿到对应标签栏列表liveClassBean
  1. private void initIndicator(List<LiveClassBean>liveClassBeanList) {
  2.     if(mIndicatorList!=null||mViewPager.getChildCount()>1){//如果页面已经初始化了就返回,避免重复初始化
  3.         return;
  4.     }
  5.     if(mIndicatorList==null){//因为一开始标签列表就是空的,所以先创建关注和精选
  6.        mIndicatorList=new ArrayList<>();
  7.        LiveClassBean liveClassBean=new LiveClassBean();
  8.        liveClassBean.setId(LiveClassBean.FOLLOW);//关注
  9.        liveClassBean.setName(getString(R.string.follow));
  10.        mIndicatorList.add(liveClassBean);
  11.        liveClassBean=new LiveClassBean();
  12.        liveClassBean.setId(LiveClassBean.FEATURED);//精选
  13.        liveClassBean.setName(getString(R.string.featured));
  14.        mIndicatorList.add(liveClassBean);
  15.     }
  16.     if(liveClassBeanList!=null){//这个时候因为已经创建了关注和精选了,所以又不是空的,直接把网络获取的标签列表都加进去
  17.        mIndicatorList.addAll(liveClassBeanList);//就导致多添加了一次关注和精选
  18.     }
  19.     List<HomeLiveViewProxy> viewProxyList=initLiveViewList();//基础标签栏列表加载完成后执行方法
  20.     ViewProxyPageAdapter pageAdapter = new ViewProxyPageAdapter(getViewProxyChildMannger(),viewProxyList);
  21.     mViewPager.setOffscreenPageLimit(viewProxyList.size());//设置 ViewPager 的 offscreenPageLimit 为 viewProxyList 的大小。这意味着 ViewPager 会预加载所有页面,避免在切换页面时出现空白或加载延迟。
  22.     CommonNavigator commonNavigator = new CommonNavigator(getActivity());//MagicIndicator的一种标签栏类型(总框架,包括宽度、对齐方式、标签的动画效果,标签大小,指示器样式啥的)
  23.     MainNavigatorAadpter mainNavigatorAadpter=new MainNavigatorAadpter(mIndicatorList,getActivity(),mViewPager);//MainNavigatorAadpter(自定义的适配器继承CommonNavigatorAdapter,用于控制标签栏的显示样式和点击事件,确保 MagicIndicator 的标签内容与 ViewPager 页面内容保持同步。)
  24.     mainNavigatorAadpter.setEableScale(false);//设置标签栏不启用缩放效果,即标签选中时不会缩放。
  25.     commonNavigator.setAdapter(mainNavigatorAadpter);//设置标签栏的适配器为mainNavigatorAadpter
  26.     mIndicator.setNavigator(commonNavigator);//MagicIndicator 提供了多种类型的 Navigator(比如 CommonNavigator、LineNavigator、CircleNavigator 等),而 CommonNavigator 只是其中的一种,所以需要MagicIndicator来进一步封装
  27.     ViewPagerHelper.bind(mIndicator, mViewPager);//当滑动不同页面时,ViewPagerHelper更新标签
  28.     pageAdapter.attachViewPager(mViewPager,1);
  29. }
复制代码
留意initIndicator()方法执行到
  1. List<HomeLiveViewProxy> viewProxyList=initLiveViewList();//基础标签栏列表加载完成后执行方法
复制代码
initLiveViewList()的时间,initLiveViewList()就是把mIndicatorList每个标签对应的直播间列表生成出来(根据标签id调用api获取对应的直播列表,每个列表元素是HomeLiveViewProxy,通过调用适配器HomeLiveAdapter,可以把api获取到的数据跟每个直播间item视图绑定起来)。api获取到每个标签栏直播间列表的数据就是一个HomeLiveViewProxy
  1. private RxRefreshView<LiveBean> mRefreshView;//一个支持刷新加载的 UI 组件(能刷新的容器)
  2. private HomeLiveAdapter mHomeLiveAdapter;//每个直播间item适配器,给每个LiveBean元素都适配成一个直播间item
复制代码
每个HomeLiveViewProxy就是一个页面,但是不是viewpaper,以是要把它转成viewpaper然后跟MagicIndicator联动
把他转成mViewPager。应该说是把HomeLiveViewProxy内容都附加到mViewPager上。而把每页数据附加到一个多页控件上就需要适配器。
  1. ViewProxyPageAdapter pageAdapter = new ViewProxyPageAdapter(getViewProxyChildMannger(),viewProxyList);// ViewPager 不直接知道怎么管理这些 HomeLiveViewProxy 页面。所以我们用 ViewProxyPageAdapter 这个“中介”告诉 ViewPager:
复制代码
获取到每个标签的HomeLiveViewProxy再给每个HomeLiveViewProxy加一个检查器(直播间列表的直播间检查presenter)
  1. private LiveRoomCheckLivePresenter mCheckLivePresenter;
  2. if(mCheckLivePresenter==null){
  3.    mCheckLivePresenter=new LiveRoomCheckLivePresenter(getActivity(),this);
  4. }
  5. for(HomeLiveViewProxy viewProxy:list){
  6.     viewProxy.setCheckLivePresenter(mCheckLivePresenter);//每个标签设置一个直播间列表的直播间检查presenter
  7. }
复制代码

加入变成列表list,返回list
  1. private List<HomeLiveViewProxy> initLiveViewList() {
  2.   RecyclerView.RecycledViewPool recycledViewPool=new RecyclerView.RecycledViewPool();
  3.   List<HomeLiveViewProxy>list=new ArrayList<>();
  4.   for(final LiveClassBean liveClassBean:mIndicatorList){//对每个标签栏创建对应的直播间列表
  5.        int id=liveClassBean.getId();//根据每个标签栏的id来创建对应的标签栏对应的直播间列表
  6.       HomeLiveViewProxy homeLiveViewProxy=null;
  7.        if(id==LiveClassBean.FEATURED){//如果是精选
  8.        homeLiveViewProxy=new HomeLiveViewProxy() {
  9.              @Override
  10.              public Observable<List<LiveBean>> getData(int p) {
  11.                  return MainAPI.getFeatured(p).map(new Function<FeatureBean, List<LiveBean>>() {
  12.                      @Override
  13.                      public List<LiveBean> apply(FeatureBean featureBean) throws Exception {
  14.                          return featureBean.getList();
  15.                      }
  16.                  });
  17.              }
  18.          };
  19.        }else if(id==LiveClassBean.FOLLOW){//如果是关注
  20.            homeLiveViewProxy=new HomeLiveViewProxy() {
  21.                @Override
  22.                public Observable<List<LiveBean>> getData(int p) {
  23.                    return MainAPI.getLiveListByFollow(p);
  24.                }
  25.            };
  26.        }else{//如果是其他标签,则根据标签ID获取对应直播间列表
  27.            homeLiveViewProxy=new HomeLiveViewProxy(){
  28.                @Override
  29.                public Observable<List<LiveBean>> getData(int p) {
  30.                 return MainAPI.getLiveListByClass(liveClassBean.getId(),p);//根据标签ID获取对应直播间列表
  31.                }
  32.            };
  33.        }
  34.       homeLiveViewProxy.setRecycledViewPool(recycledViewPool);// 为 homeLiveViewProxy 设置复用池 recycledViewPool(recycledViewPool用于缓存和复用 RecyclerView.ViewHolder)
  35.       list.add(homeLiveViewProxy);//当有多个 RecyclerView(如多个页面的列表)时,它们可以共享 recycledViewPool,避免频繁创建和销毁 ViewHolder
  36.   }
  37.     if(mCheckLivePresenter==null){
  38.        mCheckLivePresenter=new LiveRoomCheckLivePresenter(getActivity(),this);
  39.     }
  40.     for(HomeLiveViewProxy viewProxy:list){
  41.         viewProxy.setCheckLivePresenter(mCheckLivePresenter);//每个标签设置一个直播间列表的直播间检查presenter
  42.     }
  43.     return list;
  44. }
复制代码
最核心的9行代码 
  1.     ViewProxyPageAdapter pageAdapter = new ViewProxyPageAdapter(getViewProxyChildMannger(),viewProxyList);
  2.     mViewPager.setOffscreenPageLimit(viewProxyList.size());//设置 ViewPager 的 offscreenPageLimit 为 viewProxyList 的大小。这意味着 ViewPager 会预加载所有页面,避免在切换页面时出现空白或加载延迟。
  3.     CommonNavigator commonNavigator = new CommonNavigator(getActivity());//MagicIndicator的一种标签栏类型(总框架,包括宽度、对齐方式、标签的动画效果,标签大小,指示器样式啥的)
  4.     MainNavigatorAadpter mainNavigatorAadpter=new MainNavigatorAadpter(mIndicatorList,getActivity(),mViewPager);//MainNavigatorAadpter(自定义的适配器继承CommonNavigatorAdapter,用于控制标签栏的显示样式和点击事件,确保 MagicIndicator 的标签内容与 ViewPager 页面内容保持同步。)
  5.     mainNavigatorAadpter.setEableScale(false);//设置标签栏不启用缩放效果,即标签选中时不会缩放。
  6.     commonNavigator.setAdapter(mainNavigatorAadpter);//设置标签栏的适配器为mainNavigatorAadpter
  7.     mIndicator.setNavigator(commonNavigator);//MagicIndicator 提供了多种类型的 Navigator(比如 CommonNavigator、LineNavigator、CircleNavigator 等),而 CommonNavigator 只是其中的一种,所以需要MagicIndicator来进一步封装
  8.     ViewPagerHelper.bind(mIndicator, mViewPager);//当滑动不同页面时,ViewPagerHelper更新标签
  9.     pageAdapter.attachViewPager(mViewPager,1);//将 viewProxyList 通过 pageAdapter 附加到 ViewPager 上。attachViewPager() 将实际页面(即 HomeLiveViewProxy 实例)绑定到 ViewPager 中,让它能够真正展示页面内容。
复制代码
这里是先把标签和viewpaper的框架先做好再给每个viewpaper附加对应的mViewPager页面(mHomeLiveViewProxy转换成的)
  1. pageAdapter.attachViewPager(mViewPager,1);//将 viewProxyList 通过 pageAdapter 附加到 ViewPager 上。attachViewPager() 将实际页面(即 HomeLiveViewProxy 实例)绑定到 ViewPager 中,让它能够真正展示页面内容。
复制代码
把框架做好了,末了就用适配器把viewProxyList适配给mViewPager(末了这句才是真正把内容贴上去每个viewpager,前面mViewPager还是一个空白页,只不过是有对应标签的空白页)
至于为什么是pageAdapter.attachViewPager而不是mViewPager.setAdapter,因为pageAdapter.attachViewPager已经把包括ViewPager.setAdapter的都封装好了
  1. public void attachViewPager(@Nullable ViewPager viewPager,int position){
  2.     L.e("attachViewPager执行了");
  3.     mInstantiatePostion=position;
  4.     viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
  5.         @Override
  6.         public void onPageScrolled(int i, float v, int i1) {
  7.         }
  8.         @Override
  9.         public void onPageSelected(int i) {
  10.             if(mCurrentBaseViewProxy!=null){
  11.                mCurrentBaseViewProxy.setUserVisibleHint(false);
  12.             }
  13.                mCurrentBaseViewProxy=mViewList.get(i);
  14.                mCurrentBaseViewProxy.setUserVisibleHint(true);
  15.         }
  16.         //页面切换时的逻辑
  17.         //每次切换页面时,先将上一个页面 mCurrentBaseViewProxy 标记为不可见(setUserVisibleHint(false))。
  18.         //然后将当前页面 mCurrentBaseViewProxy 设置为可见(setUserVisibleHint(true))。
  19.         @Override
  20.         public void onPageScrollStateChanged(int i) {
  21.         }
  22.     });
  23.     viewPager.setAdapter(this);//这里也已经把setAdapter做了
  24.     viewPager.setCurrentItem(position);//attachViewPager() 会直接让 ViewPager 显示到指定的初始位置 position = 1,
  25.     //这样无需额外写 mViewPager.setCurrentItem(1);。
  26. }
复制代码
mViewPager.setAdapter(pageAdapter); 仅仅做了一件事:


  • 将 pageAdapter 作为适配器绑定到 ViewPager 上,以便提供数据源和页面布局。
但是它没有做:

  • 页面切换逻辑的控制




    • 没有额外设置页面切换时需要触发的逻辑(比如监听页面切换,做额外数据加载或者 UI 更新)。


  • 生命周期管理




    • ViewPager 默认只负责页面的体现和隐藏,不会管理复杂的生命周期逻辑,比如页面的“可见性标记”。


  • 默认选中页面




    • ViewPager 默认选中的是第 0 页,需要手动调用 setCurrentItem()。



  • 直接 setAdapter(): 只是简朴把书架(ViewPager)装满书(pageAdapter),但你得自己记着怎么找书和什么时间更新书。


  • 用 attachViewPager(): 不仅帮你装好书,还贴上标签(切换监听)、标记推荐书页(默认选中页面),并附带阐明书告诉你如何管理每本书的生命周期(页面可见性)。
总结


  • 目次制作:

    • MagicIndicator 相称于 目次,用来展示章节标签,让用户能快速跳转到对应的内容页面。



  • 活页空白纸加上去:

    • ViewPager 和 ViewProxyPageAdapter 就是 活页纸的框架 和适配器,把空白页面先安排好,以便后续添补内容。



  • 写内容到纸上:

    • HomeLiveViewProxy 代表 每个页面的现实内容。通过 attachViewPager() 把这些内容添补到对应的活页页面上。


商品分类界面


初始布局设置

  1. private RecyclerView mReclyviewNavigation;//左边列表视图
  2. private RecyclerView mReclyviewClassify;//右边列表视图
  3. private ClassifyIndexAdapter mClassifyIndexAdapter;//左侧列表适配器
  4. private ClassifyAdapter mClassifyAdapter;//右边列表适配器
  5. private GridLayoutManager mGridLayoutManager;//网格列表布局
  6. private EditText mEtSearch;//搜索框
  7. LinearLayoutManager linearLayoutManager=new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false);
  8. mClassifyIndexAdapter=new ClassifyIndexAdapter(null);//左侧列表适配器
  9. mReclyviewNavigation.setAdapter(mClassifyIndexAdapter);
  10. mReclyviewNavigation.setLayoutManager(linearLayoutManager);
  11. initSearch();
  12. mGridLayoutManager=new GridLayoutManager(getActivity(),3);//网格布局 展示商品分类,每行显示 3 列
  13. initClassifyReclyView();
  14. private void initSearch() {//搜索框初始化
  15.     mEtSearch = findViewById(R.id.et_search);
  16.     mEtSearch.setHint(R.string.search_goods);
  17.     ViewUtil.setEditextEnable(mEtSearch);
  18.     mEtSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
  19.         @Override
  20.         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {//当按下搜索按钮时
  21.             if (actionId == EditorInfo.IME_ACTION_SEARCH) {
  22.                 forwardSearch(v.getText().toString());//跳转到搜索界面,传入输入的字符串
  23.                 v.clearFocus();
  24.                 return true;
  25.             }
  26.             return false;
  27.         }
  28.     });
  29. }
  30. /*初始化右边列表*/
  31. private void initClassifyReclyView() {
  32.     mReclyviewClassify.setLayoutManager(mGridLayoutManager);//把网格布局管理者放进去
  33.     mClassifyAdapter=new ClassifyAdapter(R.layout.item_recly_section_classify_normal,R.layout.item_recly_section_classify_head,null);
  34.     //第一个layout布局是普通项,第二个layout布局是头部项,第三个是数据列表,这里是null后面再设置
  35.     //自定义列表适配器
  36.     mClassifyAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {//如果点到右侧列表的商品时
  37.         @Override
  38.         public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
  39.              if(mClassifyAdapter==null){
  40.                     return;
  41.               }
  42.             ClassifySectionBean sectionBean=mClassifyAdapter.getItem(position);//点击时拿到商品item索引
  43.             if(!sectionBean.isHeader&&sectionBean.t!=null){
  44.               ClassifyBean classifyBean=  sectionBean.t;
  45.               GoodsSearchArgs goodsSearchArgs=new GoodsSearchArgs();
  46.               goodsSearchArgs.cid=classifyBean.getPid();//把信息传到goodsSearchArgs对象
  47.               goodsSearchArgs.sid=classifyBean.getId();
  48.               goodsSearchArgs.className=classifyBean.getName();
  49.               ShopSearchActivity.forward(getActivity(),goodsSearchArgs);//获取对应item信息,传递搜索参数跳转到 ShopSearchActivity
  50.             }
  51.         }
  52.     });
复制代码
此中R.id.vp_search_container是继续布局,以是这个页面一开始就已经把顶部搜索框、左边列表、右边列表,基础框架做好了,之后就是往左边列表的适配器加标签栏数据,往右边的适配器加网格管理器和数据就行
留意这里的ClassifySectionBean 是extends SectionEntity<ClassifyBean>
而这个SectionEntity的泛类T是通过sectionBean.t来获取
  1. public abstract class SectionEntity<T> implements Serializable {
  2.     public boolean isHeader;
  3.     public T t;
  4.     public String header;
  5.     public SectionEntity(boolean isHeader, String header) {
  6.         this.isHeader = isHeader;
  7.         this.header = header;
  8.         this.t = null;
  9.     }
  10.     public SectionEntity(T t) {
  11.         this.isHeader = false;
  12.         this.header = null;
  13.         this.t = t;
  14.     }
  15. }
复制代码
左右列表

标签分类情况:每个标签栏是一个ClassifyBean,而ClassifyBean.children是一个List
在mData和info的时间:每个标签栏包含一个子商品列表,而initSectionBeanData里面的
List info是引用类型,以是mData=info实在这两个变量都是指向同一个List,以是在initSectionBeanData是给mData里的每个classifyBean都设置了标签
  1. 给标签头设置index
  2. ClassifyBean classifyBeanParent=info.get(i);//info.get(i)是一个classifyBean对象,所以还是引用类型
  3. classifyBeanParent.setIndex(index);//给每个标签设置索引。(给mData每个classifyBean(标签头)设置index)
  4. 给标签的每个子商品设置索引
  5. List<ClassifyBean> childArray=classifyBeanParent.getChildren();//获取当前标签的子商品列表
  6. for(ClassifyBean classifyBeanChild :childArray){
  7.     classifyBeanChild.setIndex(index);//给每个子商品设置索引。因为是引用类型,所以这里setIndex之后mData的ClassifyBean也是可以getIndex
  8.     index++;
  9. }
复制代码
在mData:标签和子商品列表是上下级关系
这里是修改了List mData,标签头是mData.get(position)。
而标签下的子商品列表是mData.get(position).getChildren()
  1. //设置标签栏封装成classifySectionBean类,装入sectionBeanList
  2. List<ClassifySectionBean> sectionBeanList=new ArrayList<>();
  3. ClassifySectionBean classifySectionBean=new ClassifySectionBean(true,classifyBean.getName());
  4. classifySectionBean.setIndex(i);//设置右侧标签头索引(注意是从0开始,且索引是i而不是index),为了对应左侧导航栏的分类标签顺序(一开始看成index了)
  5. sectionBeanList.add(classifySectionBean);//把标签头封装成 ClassifySectionBean 对象,并添加到 sectionBeanList 中
  6. //把子商品列表的每个商品封装成classifySectionBean类,装入sectionBeanList
  7. for(ClassifyBean classifyBeanChild :childArray){
  8.     classifySectionBean=new ClassifySectionBean(classifyBeanChild);//把子商品封装成 ClassifySectionBean 对象,并添加到 sectionBeanList 中
  9.     sectionBeanList.add(classifySectionBean);
  10. }
  11. if(mClassifyAdapter!=null){
  12.    mClassifyAdapter.setData(sectionBeanList);//把sectionBeanList装入右侧列表适配器
  13. }
复制代码
在这个部分由classifySectionBean封装的标签头、商品都是同一个类,就是平级关系,因为是为了右侧布局管理,每个标签头或者商品都是一个独立地区,没有包含关系。
左侧列表的List<ClassifyBean> mData的层级关系:标签栏和子商品是上下级


右侧List<ClassifySectionBean> sectionBeanList 层级关系:标签头和商品是平级关系

 左侧标签栏和右侧列表的联动

左侧标签栏点击,右侧滑动到相应标签头
  1. mClassifyIndexAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
  2.     @Override
  3.     public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
  4.        classifyScrollPosition(position);//当点击左侧标签栏时调用 classifyScrollPosition() 方法滚动右侧列表。
  5.     }
  6. });
  7. private void classifyScrollPosition(int position) {//滚动分类列表到指定位置。
  8.     if(mData==null){
  9.         return;
  10.     }
  11.     ClassifyBean classifyBean=mData.get(position);
  12.     mGridLayoutManager.scrollToPositionWithOffset(classifyBean.getIndex(), 0);//用classifyBean的index转到对应的layout区域
  13.     //他是拿到标签头的右侧索引然后用layoutManager滑动到对应布局的位置,应该是右侧的布局排序是按照classifyBean的index排序而不是ClassifySectionBean的Index
  14. }
复制代码
右侧滑动,左侧标签栏转到对应标签
  1. mReclyviewClassify.addOnScrollListener(new RecyclerView.OnScrollListener() {//右侧列表滑动监听
  2.     private int mState;
  3.     @Override
  4.     public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState){
  5.         mState=newState;
  6.     }
  7.     @Override
  8.     public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
  9.         if (mState == RecyclerView.SCROLL_STATE_IDLE) {
  10.             return;
  11.         }
  12.         L.e("dy=="+dy);
  13.         //前面滑动的时候:左侧标签选中第一个可见项的那个标签头,第一个可见项不是标签头就不管
  14. 意味着只要每次有标签头经过屏幕界面上方,标签栏就会更新成经过上方的那个标签
  15.         int firstPosition=mGridLayoutManager.findFirstVisibleItemPosition();//获取当前第一个可见项位置
  16.         if(mClassifyAdapter==null||mClassifyIndexAdapter==null){
  17.             return;
  18.         }
  19.         ClassifySectionBean sectionBean=mClassifyAdapter.getData().get(firstPosition);
  20.         if(sectionBean.isHeader){
  21.             mClassifyIndexAdapter.setSelectIndex(sectionBean.getIndex());//获取当前第一个可见项位置,通过 setSelectIndex() 更新左侧导航选中状态
  22.         }
  23.         //当滑动到最底下:如果屏幕滑动到最底下,左侧标签栏选中为最后一个标签。因为一个页面有好几个标签头,
  24.         //最底下的标签头肯定摸不到屏幕上方,所以他的标签肯定选中不了。所以设置成这样会好看一些。
  25.     int lastCompleteVisibleItemPosition=mGridLayoutManager.findLastCompletelyVisibleItemPosition();//找到当前最后一个完全可见项位置
  26.     if ( lastCompleteVisibleItemPosition>=mClassifyAdapter.size() - 1) {//如果最后一个完全可见项位置大于等于整个布局列表的最后一个(意思就是如果滑到最底下的时候),则认为已经滑动到最后了
  27.         mClassifyIndexAdapter.setSelectIndex(mClassifyIndexAdapter.size()-1);//滑动到最后就要把左侧标签选中位置设置为最后一个
  28.         // (因为一页有好几个标签头,不这样设置,滑到最后标签肯定不会选中最后一个标签)
  29.     }
  30.     }
  31. });
复制代码


商品界面 GoodsDetailActivity

商品标签GoodsPannelViewProxy

初始化轮播图

  1. mBanner.setAutoPlay(false).setIndicator(defaultIndicator())//适配指示器
  2.         .setAdapter(mImageAdapter);//适配器
  3.         
  4. mImageAdapter.setData(bannerList);//把图片数据装入适配器
复制代码
商品标签大部分简朴数据直接从mStoreGoodsBean获取

  1. mTvPrice.setText(StringUtil.getPrice(mStoreGoodsBean.getPrice()));
  2. mTvOtPrice.setText(getString(R.string.original_tip,mStoreGoodsBean.getOriginalPrice()));
  3. mTvTitle.setText(mStoreGoodsBean.getName());
  4. String uint=mStoreGoodsBean.getUnitName();
  5. mTvStock.setText(getString(R.string.stock_tip,mStoreGoodsBean.getStock(),uint));
  6. mTvSaleNum.setText(getString(R.string.sale_num_tip,mStoreGoodsBean.getSales(),uint));
复制代码
SpecsSelectViewProxy商品规格选择


  1. //规格选择难点:规格尺寸适配器
  2. SpecSelectAdapter adapter = new SpecSelectAdapter(specsBeanList,key, getViewProxyMannger().getLayoutInflater());
复制代码

初始化:获取默认选项,适配每一类的规格选择框(layoutPosition类的位置,flexRadioGroup选项的容器,labelList此类规格可选项列表)
更新变乱:
  1. public SpecSelectAdapter(List<SpecsBean> list, String selectKey, LayoutInflater layoutInflater) {
  2.     //为什么是List<SpecsBean>,因为有的商品规格要选的不仅一项,可能是两项以上,比如颜色和尺寸都要选,那就是两个specsBean
  3.     super(list);
  4.     if (selectKey != null) {
  5.         specKeyArray = selectKey.split(",");//初始化默认规格选项
  6.     }
  7.     mLayoutInflater = layoutInflater;
  8. }
  9. @Override
  10. public void convert(BaseReclyViewHolder helper, SpecsBean item) {
  11.     helper.setText(R.id.tv_title, item.getName());//规程名称(颜色、码数等)
  12.     int position = helper.getObjectPosition();//规格总布局位置(两个规格颜色和尺寸,颜色position是0,尺寸position是1)
  13.     List<String> labelList = item.getValue();
  14.     FlexRadioGroup flexRadioGroup = helper.getView(R.id.flex);
  15.     //设置 FlexRadioGroup 的布局方向和换行方式。
  16.     flexRadioGroup.setFlexWrap(FlexWrap.WRAP);
  17.     flexRadioGroup.setFlexDirection(FlexDirection.ROW);
  18.     initChild(position, flexRadioGroup, labelList);
  19. }
  20. private int getChildPosition(View view) {
  21.     Object object = view.getTag();
  22.     if (object != null && object instanceof Integer) {
  23.         return (int) object;
  24.     }
  25.     return -1;
  26. }
  27. private void initChild(final int layoutPosition, final FlexRadioGroup flexRadioGroup, final List<String> labelList) {
  28.     if (!ListUtil.haveData(labelList) || mLayoutInflater == null) {
  29.         return;
  30.     }
  31.     flexRadioGroup.setOnCheckedChangeListener(new FlexRadioGroup.OnCheckedChangeListener() {
  32.         @Override//选中项发生更改时
  33.         public void onCheckedChanged(int checkedId) {
  34.             //设置 FlexRadioGroup 的选中变化监听器,当选择发生变化时更新 specKeyArray 并调用 specKeyChange 方法。
  35.             View view = flexRadioGroup.findViewById(checkedId);
  36.             if (view == null) {
  37.                 return;
  38.             }
  39.             int childPosition = getChildPosition(view);
  40.             int size = ListUtil.getSize(specKeyArray);
  41.             if (childPosition != -1 && size > layoutPosition) {
  42.                 //更新specKeyArray当前所选规格的类中的选择项的值,比如默认[红色,XL],更改颜色规格为蓝色,则为[蓝色,XL]
  43.                 specKeyArray[layoutPosition] = labelList.get(childPosition);
  44.                 specKeyChange();
  45.             } else {
  46.                 DebugUtil.sendException("specKeyArray大小必须大于layoutPosition");
  47.             }
  48.         }
  49.     });
  50.     int size = labelList.size();
  51.     //获取并返回 specKeyArray 中 layoutPosition(当前选中位置) 位置的元素
  52.     String key = ListUtil.getArrayData(specKeyArray, layoutPosition);
  53.     for (int i = 0; i < size; i++) {
  54.         String label = labelList.get(i);
  55.         boolean isChecked = false;//初始状态为未点击
  56.         if (StringUtil.equals(key, label)) {
  57.             isChecked = true;//如果当前要设置的键与选中的键相同,则设置为点击选中状态
  58.         }
  59.         //遍历标签列表,创建 RadioButton 并设置其文本、标签和初始选中状态,然后添加到 FlexRadioGroup 中。
  60.         RadioButton radioButton = (RadioButton) mLayoutInflater.inflate(R.layout.item_relcy_spec_child, flexRadioGroup, false);
  61.         radioButton.setText(label);//设置每个可选规格按键的值
  62.         radioButton.setTag(i);//设置标签
  63.         flexRadioGroup.addView(radioButton);//把按键加到flexRadioGroup容器
  64.         radioButton.setChecked(isChecked);//初始状态
  65.     }
  66. }
复制代码
评价标签GoodsEvaluateViewProxy

评价列表数据为goodsParseBean.getReply()
  1. mGoodsId=goodsParseBean.getGoodsInfo().getId();
  2. mTvEvaluateNum.setText(getString(R.string.evaluate_tip,goodsParseBean.getReplyCount()));//获取评价数量(goodsParseBean.getReplyCount())
  3. mTvFeedbackRate.setText(goodsParseBean.getReplyChance()+"%");//获取好评率(goodsParseBean.getReplyChance())
  4. EvaluateBean2 evaluateBean=goodsParseBean.getReply();
  5. if(evaluateBean!=null){
  6.    mEvaluateLinearListAdapter.setData(ListUtil.asList(evaluateBean));
  7. }
复制代码
EvaluateListActivity评价具体界面


核心代码:refreshview视图和列表适配器、点击评价类型革新数据、革新视图、上划下拉革新视图
  1. // 初始化总评分星星的正常和选中状态图片
  2. int size = DpUtil.dp2px(10);
  3. Bitmap starNormal = BitmapUtil.thumbImageWithMatrix(getResources(), R.drawable.icon_evaluate_default, size, size);
  4. Bitmap starFocus = BitmapUtil.thumbImageWithMatrix(getResources(), R.drawable.icon_evaluate_select, size, size);
  5. mStar.setNormalImg(starNormal);
  6. mStar.setFocusImg(starFocus);
  7. // 设置评价类型按钮组的监听器
  8. mVpBtnEvaluate.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
  9.     @Override
  10.     public void onCheckedChanged(RadioGroup group, int checkedId) {
  11.         // 根据选中的按钮切换评价类型
  12.         if(checkedId==R.id.btn_total){
  13.             checkType(ShopState.COMMENTS_TOTAL);
  14.         }else if(checkedId==R.id.btn_best){
  15.             checkType(ShopState.COMMENTS_GOODS);
  16.         }else if(checkedId==R.id.btn_normal){
  17.             checkType(ShopState.COMMENTS_NORMAL);
  18.         }else if(checkedId==R.id.btn_bad){
  19.             checkType(ShopState.COMMENTS_BAD);
  20.         }
  21.     }
  22. });
  23. mEvaluateListAdapter=new EvaluateListAdapter(null,getResources());
  24. mEvaluateListAdapter.setStringOnItemClickListener(new ViewGroupLayoutBaseAdapter.OnItemClickListener<String>() {
  25.     @Override
  26.     public void onItemClicked(ViewGroupLayoutBaseAdapter<String> adapter, View v, String item, int position) {
  27.         // 点击评价中的图片时显示图片画廊
  28.         List<String>urlList=adapter.getData();
  29.         showGally(urlList,position);
  30.     }
  31. });
  32. // 设置刷新视图的适配器和布局管理器
  33. mRefreshView.setAdapter(mEvaluateListAdapter);
  34. mRefreshView.setReclyViewSetting(RxRefreshView.ReclyViewSetting.createLinearSetting(this,1));
  35. mRefreshView.setDataListner(new RxRefreshView.DataListner<EvaluateBean>() {
  36.     @Override//适配器的data不是在新建适配器的时候放入,而是使用dataListener使用getData方法从服务器获取数据
  37.     //当initData、下拉或者上划时,会调用loadData方法,并传入当前页码p,然后返回一个Observable对象,
  38.     public Observable<List<EvaluateBean>> loadData(int p) {
  39.         // 加载评价数据
  40.         //这个方法负责从服务器获取评价数据,并返回一个 Observable<List<EvaluateBean>>。
  41.         // 当数据加载完成后,RxRefreshView 会调用 compelete 方法来更新适配器。
  42.         return getData(p);
  43.     }
复制代码
推荐标签 GoodsDetailRecommendViewProxy


大概流程:拿到List<GoodsBean>分组,分成n组,每组6个GoodsBean,拿到 List<List<GoodsBean>>给轮播图适配器BannerAdapter,BannerAdapter里面调用initReclyView设置好每页RecyclerView的布局,每页一个List<GoodsBean>,再把List<GoodsBean>分给GoodsAdapter做好每个商品GoodsBean的适配

第一层:List ==>> List> 给数据分组,每组6个
  1. public void setData(List<GoodsBean>data){
  2.     if(!ListUtil.haveData(data)||mBannerAdapter==null){
  3.         return;
  4.     }
  5.     if(mData!=null){
  6.         mData.clear();
  7.     }else{
  8.         mData=new ArrayList<>();
  9.     }
  10.     //处理数据,分割成多个子列表
  11.     int size=data.size();
  12.     int totalCount=size/mSpanCount;//总页数   其中mSpanCount=6
  13.     int remainder=size%mSpanCount;//余数、最后一页的个数
  14.     if(remainder>0){
  15.         totalCount=totalCount+1;
  16.     }
  17.     for(int i=0;i<totalCount;i++){
  18.         int tempStart=i*mSpanCount;
  19.         if(tempStart<0){
  20.             tempStart=0;//每一页的起始地址
  21.         }
  22.         int tempEnd=(i+1)*mSpanCount;
  23.         if(tempEnd>=size){
  24.             tempEnd=size;//每一页的结束地址
  25.         }
  26.         //每个subList就是一页6个商品的列表
  27.         List<GoodsBean>subList=data.subList(tempStart,tempEnd);//获取从起始地址到结束地址的6个商品作为一个subList
  28.         mData.add(subList);
  29.     }
  30.     mBannerAdapter.setData(mData);//第一层List<List<GoodsBean>>
  31. }
复制代码
第二层:每页是一个List<GoodsBean>,BannerAdapter是每个页面的适配器,负责把拿到的List>分解成每页一个List,调用initReclyView初始化每页的页面布局,然后把当前页面的List数据给GoodsAdapter
  1. public class BannerAdapter extends BaseRecyclerAdapter<List<GoodsBean>,BaseReclyViewHolder> {
  2.     public BannerAdapter(List<List<GoodsBean>> data) {
  3.         super(data);
  4.     }
  5.     //转换视图,设置数据
  6.     @Override
  7.     protected void convert(@NonNull BaseReclyViewHolder helper, List<GoodsBean> item) {//第二层List<GoodsBean>
  8.         RecyclerView recyclerView= helper.getView(R.id.reclyView);
  9.         GoodsAdapter adapter= (GoodsAdapter) recyclerView.getAdapter();
  10.         if(adapter==null){
  11.             adapter= initReclyView(recyclerView);
  12.         }
  13.         adapter.setData(item);
  14.     }
  15.     //获取布局ID
  16.     @Override
  17.     public int getLayoutId() {
  18.         return R.layout.item_relcy;
  19.     }
  20. }
复制代码
initReclyView封装功能:


  • 把每页的RecyclerView设置适配器GoodsAdapter
  • 设置好每页的布局(网格布局一页每行3个)、分割线
  • 设置goodsAdapter点击变乱
  1. //初始化RecyclerView
  2. private GoodsAdapter initReclyView(RecyclerView recyclerView) {
  3.     GoodsAdapter goodsAdapter=new GoodsAdapter(null);
  4.     recyclerView.setAdapter(goodsAdapter);
  5.     //配置LayoutManager,每行3个,一页展示6个,所以是2行
  6.     GridLayoutManager gridLayoutManager= new GridLayoutManager(getActivity(),3){//每行3个
  7.         @Override
  8.         public boolean canScrollVertically() {
  9.             return false;
  10.         }
  11.     };
  12.     //添加分割线
  13.     ItemDecoration decoration = new ItemDecoration(getActivity(), 0xffdd00, 10, 10);
  14.     //recyclerView是引用类型,所以这里修改recyclerView会直接影响到BannerAdapter的recyclerView
  15.     recyclerView.setLayoutManager(gridLayoutManager);
  16.     recyclerView.addItemDecoration(decoration);
  17.     //设置点击事件监听器
  18.     goodsAdapter.setOnItemClickListener(GoodsDetailRecommendViewProxy.this);
  19.     return goodsAdapter;
  20. }
复制代码
第三层:GoodsAdapter是每个页面中每个商品项的适配器。把当前页的List的6个GoodsBean做适配。负责把拿到的List分成每个商品项GoodsBean适配
  1. public class GoodsAdapter extends BaseRecyclerAdapter<GoodsBean,BaseReclyViewHolder>{
  2.     public GoodsAdapter(List<GoodsBean> data) {
  3.         super(data);
  4.     }
  5.     //获取布局ID
  6.     @Override
  7.     public int getLayoutId() {
  8.         return R.layout.item_recly_goods_recommend;
  9.     }
  10.     //转换视图,设置数据
  11.     @Override
  12.     protected void convert(@NonNull BaseReclyViewHolder helper, GoodsBean item) {//第三层GoodsBean
  13.         helper.setImageUrl(item.getThumb(),R.id.img_cover);
  14.         helper.setText(R.id.tv_title,item.getName());
  15.         helper.setText(R.id.tv_price,item.getUnitPrice());
  16.     }
  17. }
复制代码

商品详情GoodsWebViewProxy

核心内容:
加载 HTML 内容
loadHtml(String html):
提取 HTML 中所有图片的 URL,并存储在 imageUrls 列表中。
替换 标签样式以确保图片不会超出 WebView 宽度。
使用 loadDataWithBaseURL 方法将 HTML 内容加载到 WebView 中。
  1. public void loadHtml(String html){//html: 要加载的 HTML 字符串
  2.     if(mOpenImageJavaInterface!=null){
  3.         //提取图片 URL: 从 HTML 中提取所有图片的 URL,并存储在 OpenImageJavaInterface 中。
  4.         mOpenImageJavaInterface.imageUrls=StringUtil.returnImageUrlsFromHtml(html);
  5.     }
  6.     if(mWebView!=null){
  7.         //替换 img 标签样式: 确保图片在 WebView 中显示时不会超出容器宽度
  8.         html = html.replace("<img", "<img style="display:        ;max-width:100%;"");
  9.         //加载 HTML 数据: 使用 loadDataWithBaseURL 方法将 HTML 内容加载到 WebView 中。
  10.         mWebView.loadDataWithBaseURL("about:blank", html, mimeType,
  11.                 encoding, "");
  12.     }
  13. }
复制代码
处置惩罚图片点击变乱
addImageClickListener(WebView webView): 通过 JavaScript 注入代码为页面中的所有图片添加点击变乱监听器,当用户点击图片时调用 openImage 方法。
openImage(String src) (位于 OpenImageJavaInterface 类中): 处置惩罚图片点击变乱,查找点击的图片 URL 在 imageUrls 列表中的位置,并体现大图预览对话框。
体现大图预览对话框
showGalleryDialog(int position): 创建并体现图片大图预览对话框,使用 FragmentActivity 的 SupportFragmentManager 体现对话框。


直播间界面LiveAudienceActivity


  1. private MyViewPager mViewPager;//两个页面,一个空界面一个ui界面,可左右滑动
  2. private ViewGroup mSecondPage;//默认显示第二页(包含了底层和顶层布局)
  3. private LiveAudienceViewHolder mLiveAudienceViewHolder;// 初始化观众直播间底部视图(聊天、礼物、分享、点赞飘心心)
  4. protected LiveRoomViewHolder mLiveRoomViewHolder;// 初始化直播间顶层页面框架(评论、主播头像、点赞量、本场在售商品、在线观众、退出)
  5. /**
  6. * 主函数入口
  7. * 初始化音频流、界面布局和视图持有者
  8. */
  9. @Override
  10. protected void main() {
  11.     // 确保音量控制器在播放音乐时控制音量,而不是系统的其他音量
  12.     setVolumeControlStream(AudioManager.STREAM_MUSIC);
  13.     // 如果使用滚动布局
  14.     if (isUseScroll()) {
  15.         // 初始化RecyclerView并设置固定大小和垂直线性布局管理器
  16.         mRecyclerView = super.findViewById(R.id.recyclerView);
  17.         mRecyclerView.setHasFixedSize(true);
  18.         mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));
  19.         // 为直播观众界面布局
  20.         mMainContentView = LayoutInflater.from(mContext).inflate(R.layout.activity_live_audience, null, false);
  21.     }
  22.     // 调用父类的main方法进行初始化
  23.     super.main();
  24.     // 初始化播放容器和主容器
  25.     mPlayContainer = (ViewGroup) findViewById(R.id.play_container);//总直播间容器
  26.     mContainer = (FrameLayout) findViewById(R.id.container);
  27.     // 初始化直播播放视图持有者并添加到父容器
  28.     mLivePlayViewHolder = new LivePlayTxViewHolder(mContext, mPlayContainer);
  29.     mLivePlayViewHolder.addToParent();
  30.     //让这个视图持有者订阅 Activity 的生命周期,确保在活动的生命周期内适时地清理和更新视图。
  31.     mLivePlayViewHolder.subscribeActivityLifeCycle();
  32.     // 初始化ViewPager和第二个页面布局
  33.     mViewPager = (MyViewPager) findViewById(R.id.viewPager);
  34.     mSecondPage = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.view_audience_page, mViewPager, false);
  35.     mContainerWrap = mSecondPage.findViewById(R.id.container_wrap);
  36.     //mContainer是mSecondPage的组件,包含LiveRoomViewHolder和LiveAudienceViewHolder
  37.     mContainer = mSecondPage.findViewById(R.id.container);
  38.     // 初始化直播间顶层页面框架(评论、主播头像、点赞量、本场在售商品、在线观众、退出)
  39.     mLiveRoomViewHolder = new LiveRoomViewHolder(mContext, mContainer, (GifImageView) mSecondPage.findViewById(R.id.gift_gif), (SVGAImageView) mSecondPage.findViewById(R.id.gift_svga), mContainerWrap);
  40.     mLiveRoomViewHolder.addToParent();
  41.     mLiveRoomViewHolder.subscribeActivityLifeCycle();
  42.     // 初始化观众直播间底部视图(聊天、礼物、分享、点赞)
  43.     mLiveAudienceViewHolder = new LiveAudienceViewHolder(mContext, mContainer);
  44.     mLiveAudienceViewHolder.addToParent();
  45.     mLiveAudienceViewHolder.setUnReadCount(getImUnReadCount());
  46.     mLiveBottomViewHolder = mLiveAudienceViewHolder;
  47.     // 设置ViewPager适配器
  48.     mViewPager.setAdapter(new PagerAdapter() {
  49.         @Override
  50.         public int getCount() {
  51.             return 2;
  52.         }
  53.         @Override
  54.         public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
  55.             return view == object;
  56.         }
  57.         @NonNull
  58.         @Override
  59.         public Object instantiateItem(@NonNull ViewGroup container, int position) {
  60.             if (position == 0) {
  61.                 //第一页是空View空页面,用于右滑展示单独的直播视频,
  62.                 View view = new View(mContext);
  63.                 view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
  64.                 container.addView(view);
  65.                 return view;
  66.             } else {
  67.                 //第二页是mSecondPage,意思是mSecondPage包含了所有ui按键层的布局
  68.                 container.addView(mSecondPage);
  69.                 return mSecondPage;
  70.             }
  71.         }
  72.         @Override
  73.         public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
  74.         }
  75.     });
  76.     // 设置ViewPager当前项为第二个页面
  77.     mViewPager.setCurrentItem(1);
  78.     // 初始化直播连麦相关 presenter
  79.     mLiveLinkMicPresenter = new LiveLinkMicPresenter(mContext, mLivePlayViewHolder, false, mLiveSDK, mLiveAudienceViewHolder.getContentView());
  80.     mLiveLinkMicAnchorPresenter = new LiveLinkMicAnchorPresenter(mContext, mLivePlayViewHolder, false, mLiveSDK, null);
  81.     mLiveLinkMicPkPresenter = new LiveLinkMicPkPresenter(mContext, mLivePlayViewHolder, false, null);
  82.     // 如果使用滚动布局
  83.     if (isUseScroll()) {
  84.         // 获取直播数据列表并初始化滚动适配器
  85.         List<LiveBean> list = LiveStorge.getInstance().get(mKey);
  86.         mRoomScrollAdapter = new LiveRoomScrollAdapter(mContext, list, mPosition);
  87.         mRoomScrollAdapter.setActionListener(new LiveRoomScrollAdapter.ActionListener() {
  88.             @Override
  89.             public void onPageSelected(LiveBean liveBean, ViewGroup container, boolean first) {
  90.                 L.e(TAG, "onPageSelected----->" + liveBean);
  91.                 // 当选中直播页面时,更新主内容视图的父容器
  92.                 if (mMainContentView != null && container != null) {
  93.                     ViewParent parent = mMainContentView.getParent();
  94.                     if (parent != null) {
  95.                         ViewGroup viewGroup = (ViewGroup) parent;
  96.                         if (viewGroup != container) {
  97.                             viewGroup.removeView(mMainContentView);
  98.                             container.addView(mMainContentView);
  99.                         }
  100.                     } else {
  101.                         container.addView(mMainContentView);
  102.                     }
  103.                 }
  104.             }
  105.             @Override
  106.             public void onPageOutWindow(String liveUid) {
  107.                 L.e(TAG, "onPageOutWindow----->" + liveUid);
  108.                 // 当页面移出窗口时,取消相关HTTP请求并清理房间数据
  109.                 if (TextUtils.isEmpty(mLiveUid) || mLiveUid.equals(liveUid)) {
  110.                     LiveHttpUtil.cancel(LiveHttpConsts.CHECK_LIVE);
  111.                     LiveHttpUtil.cancel(LiveHttpConsts.ENTER_ROOM);
  112.                     LiveHttpUtil.cancel(LiveHttpConsts.ROOM_CHARGE);
  113.                     clearRoomData();
  114.                 }
  115.             }
  116.         });
  117.         // 设置RecyclerView适配器
  118.         mRecyclerView.setAdapter(mRoomScrollAdapter);
  119.     }
  120.     // 如果有直播数据,则设置直播间数据并进入房间
  121.     if (mLiveBean != null) {
  122.         setLiveRoomData(mLiveBean);
  123.         enterRoom();
  124.     }
  125. }
  126. /**
  127. * 点亮飘心心,被LiveAudienceViewHolder和LiveRoomViewHolder调用
  128. */
  129. public void light() {
  130.     if (!mLighted) {
  131.         mLighted = true;
  132.         SocketChatUtil.sendLightMessage(mSocketClient, 1 + RandomUtil.nextInt(6));
  133.     }
  134.     if (mLiveRoomViewHolder != null) {
  135.         mLiveRoomViewHolder.playLightAnim();
  136.     }
  137.     setLikes();
  138. }
  139. /**  LiveAudienceViewHolder和LiveRoomViewHolder的点击事件   */
  140. @Override
  141. public void onClick(View v) {
  142.     int i = v.getId();
  143.     if (i == R.id.root) {//当点到页面上除了控件外的地方时(根布局)
  144.         light();//弹出点亮心心动画
  145.     }
  146.     if (!canClick()) {
  147.         return;
  148.     }
  149.     if (i == R.id.avatar) {
  150.         if (mLiveActivity!=null){
  151.             mLiveActivity.showAnchorUserDialog(mIsFollowAnthor);
  152.         }
  153.     } else if (i == R.id.btn_follow) {
  154.         follow();
  155.     } else if (i == R.id.btn_goods) {
  156.         openShopGoods();
  157.     }else if(i==R.id.btn_close){//点击右上角叉,退出直播间
  158.         close();
  159.     }else if(i==R.id.tv_user_count){
  160.         openUserList();
  161.     }
  162. }
复制代码
四个核心布局/容器:


  • 最外层容器:mContainer(FrameLayout),作为主容器(放全部)。
  • 第二层容器:mSecondPage,包含直播间的所有UI元素(批评、主播头像、点赞量、本场在售商品、在线观众、退出等)。
包含mLiveAudienceViewHolder(底部)和 mLiveRoomViewHolder(顶部)


  • 第三层容器:mPlayContainer,用于播放视频的容器,
  • 第四层容器:mViewPager,用于左右滑动切换不同的页面(一个空页面和一个包含所有UI元素的页面mSecondPage)。
当点击到LiveAudienceViewHolder和LiveRoomViewHolder除了控件外的地方时,弹出心心动画

悬浮窗功能checkPermissonOpenNarrow



1. 触发悬浮窗

悬浮窗的触发通常有两种方式:

  • 用户按下 Home 键
:通过 HomeWatcherReceiver 监听 Home 键变乱。

  • 用户主动触发悬浮窗(核心方法,无论如何都会调用)
:例如调用 checkPermissonOpenNarrow 方法。
1.1 Home 键触发流程


  • 当用户按下 Home 键时,系统会发送一个 ACTION_CLOSE_SYSTEM_DIALOGS 广播。
  • HomeWatcherReceiver 的 onReceive 方法会吸收到该广播,并解析 reason 字段。

    • 如果 reason 是 homekey(表现短按 Home 键),则调用 shortClick() 方法。

  • shortClick()方法会调用 checkPermissonOpenNarrow,检查悬浮窗权限并体现悬浮窗。
1.2 主动触发流程


  • 在 LiveAudienceActivity 中,调用 checkPermissonOpenNarrow 方法。

    • 该方法会检查悬浮窗权限,如果权限已授予,则直接调用 openNarrow 体现悬浮窗。
    • 如果权限未授予,则弹出对话框提示用户去设置中开启权限。

2. 检查悬浮窗权限(重点checkPermissonOpenNarrow)



  • checkPermissonOpenNarrow 方法是权限检查的入口:

    • 调用 WindowAddHelper 的 checkOverLay 方法,检查当前是否具有悬浮窗权限。

      • 如果权限已授予,直接调用 openNarrow 体现悬浮窗。
      • 如果权限未授予,调用 openMakeWindowsPermissonDialog 弹出对话框,提示用户去设置中开启权限。




  1. public void checkPermissonOpenNarrow(Context context, boolean needRequestPermisson, final boolean needLockTouch) {
  2. //needRequestPermisson是选择是否弹出请求对话框(举报界面false就不会弹出对话框,返回键和图标叉true就会弹出)。
  3. //needLockTouch如果是true:能拖不能点;如果是false:能拖能点
  4.     if (mWindowAddHelper == null) {
  5.         mWindowAddHelper = new WindowAddHelper(this);
  6.     }
  7.     mWindowAddHelper.checkOverLay(this, new Predicate<Boolean>() {
  8.         @Override
  9.         public boolean test(Boolean aBoolean) throws Exception {
  10.             if (!aBoolean) {//注意这里的aBoolean是从checkOverLay里面的openMakeWindowsPermissonDialog(context)来的
  11.                 onBackAndFinish();
  12.             }
  13.             return aBoolean;
  14.         }
  15.     }, needRequestPermisson).subscribe(new Consumer<Boolean>() {
  16.         @Override
  17.         public void accept(Boolean aBoolean) throws Exception {
  18.             if (aBoolean) {
  19.                 openNarrow(needLockTouch);
  20.             } else if (!needLockTouch) {
  21.                 onBackAndFinish();
  22.             }
  23.         }
  24.     });
  25. }
复制代码
2.1 checkOverLay 方法



  • 该方法会调用 isDrawOverLay 检查当前是否具有悬浮窗权限。

    • 如果权限已授予,返回 true。
    • 如果权限未授予,返回 false,并调用 openMakeWindowsPermissonDialog 弹出对话框。

  • openMakeWindowsPermissonDialog(context) 这个方法返回一个 Observable,它会向下游发射一个 Boolean 值,这个值就是aBoolean

  1. public Observable<Boolean> checkOverLay(Context context,Predicate<Boolean> predicate, boolean needRequestPermisson) {
  2.     //调用 isDrawOverLay 检查当前是否具有悬浮窗权限
  3.     if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  4.         boolean isDrawOverLays=isDrawOverLay();
  5.         if(!isDrawOverLays&&needRequestPermisson){
  6.             return  openMakeWindowsPermissonDialog(context).filter(predicate).flatMap(new io.reactivex.functions.Function<Boolean, ObservableSource<Boolean>>() {
  7.                 @Override//用户在openMakeWindowsPermissonDialog点击“立即开启”后,返回的aboolean是true
  8.                 //这个时候checkPermissonOpenNarrow中的Predicate<Boolean>会传入true到predicate,所以后面的flatMap会接着执行
  9.                 //调用 requestOverLay 方法,跳转到系统设置页面
  10.                 public ObservableSource<Boolean> apply(Boolean aBoolean) throws Exception {
  11.                     return requestOverLay();
  12.                 }
  13.             });
  14.         }
  15.         return  Observable.just(isDrawOverLays);
  16.     }else{
  17.         return  Observable.just(true);
  18.     }
  19. }
复制代码
也就是说filter(predicate)是一个漏斗:openMakeWindowsPermissonDialog(context)的判定影响后续是否继续执行flatMap
2.2 openMakeWindowsPermissonDialog 方法



  • 该方法会弹出一个对话框,提示用户去设置中开启悬浮窗权限。

    • 用户点击“立刻开启”后,调用 requestOverLay 方法,跳转到系统设置页面。
    • 用户点击“关闭直播间”后,直接关闭直播间。

  1. private Observable<Boolean> openMakeWindowsPermissonDialog(final Context context){
  2.     return Observable.create(new ObservableOnSubscribe<Boolean>() {
  3.         @Override
  4.         public void subscribe(ObservableEmitter<Boolean> e) throws Exception {
  5.             openMakeWindowsPermissonDialog(context,e);
  6.         }
  7.     });
  8. }
  9. private void openMakeWindowsPermissonDialog(Context context,final ObservableEmitter<Boolean> e){
  10. //弹出一个对话框,提示用户去设置中开启悬浮窗权限。
  11. //这个对话框其实就是返回一个布尔值,这个布尔值用来判断下面是否要打开悬浮窗设置
  12.     Dialog dialog= new DialogUitl.Builder(context)
  13.             .setTitle("")
  14.             .setContent("你的手机没有授权浮窗权限,是否前往申请?")
  15.             .setCancelable(true)
  16.             .setBackgroundDimEnabled(true)
  17.             .setCancelString("关闭直播间")
  18.             .setConfrimString("立即开启")
  19.             .setClickCallback(new DialogUitl.SimpleCallback2() {
  20.                 @Override
  21.                 public void onConfirmClick(Dialog dialog, String content) {
  22.                     e.onNext(true);
  23.                 }
  24.                 @Override
  25.                 public void onCancelClick() {
  26.                     e.onNext(false);
  27.                 }
  28.             })
  29.             .build();
  30.     dialog.show();
  31. }
复制代码
2.3 requestOverLay 方法



  • 该方法会跳转到系统设置页面,哀求悬浮窗权限。

    • 用户开启权限后,返回应用,再次调用 openNarrow 体现悬浮窗。

3. 体现悬浮窗



  • openNarrow
 方法是悬浮窗体现的核心逻辑:



    • 初始化悬浮窗参数







      • 调用 initWindowMannger 初始化 WindowManager。
      • 调用 exportFlowView 将当前直播窗口的内容导出为一个 View。
      • 创建 WindowManager.LayoutParams,设置悬浮窗的位置、大小和样式。





    • 延迟体现悬浮窗







      • 通过 Observable.timer 延迟肯定时间(如 1 秒)后体现悬浮窗。
      • 延迟的目的是为了避免悬浮窗立刻弹出,影响用户体验。





    • 创建悬浮窗







      • 调用 createNarrowWindow 方法,将导出的 View 添加到 FloatFrameLayout 中。
      • 通过 WindowManager.addView 将 FloatFrameLayout 添加到屏幕上,体现悬浮窗。


3.1 initWindowMannger 方法



  • 该方法会初始化 WindowManager,用于管理悬浮窗的体现和隐藏。
3.2 exportFlowView 方法



  • 该方法会将当前直播窗口的内容导出为一个 View,用于表如今悬浮窗中。
3.3 createNarrowWindow 方法



  • 该方法会创建悬浮窗:



    • 创建一个 FloatFrameLayout,用于承载悬浮窗的内容。
    • 将导出的 View 添加到 FloatFrameLayout 中。
    • 通过 WindowManager.addView 将 FloatFrameLayout 添加到屏幕上,体现悬浮窗。

反面的打开悬浮窗,附加参数和图像之类的没有涉及到RxJava而是windowManger,了解流程就行(设置悬浮窗:设置参数、直播图像、悬浮窗容器。 打开悬浮窗:将主栈顶的运动(LiveAudienceActivity)推到前台
  1. private void openNarrow(final boolean needLockTouch) {//悬浮窗显示的核心逻辑
  2.     //initReceiver();
  3.     isFloatAtWindow = true;
  4.     initWindowMannger();
  5.     final View view = exportFlowView();
  6.     final WindowManager.LayoutParams layoutParams = mWindowAddHelper.createDefaultWindowsParams(0, 100);
  7.     if (view != null) {
  8.         moveTaskToBack(false);//当前界面退到后台
  9.         int time = delayToFloatWindowTime();//默认1秒
  10.         if (time <= 0) {
  11.             createNarrowWindow(layoutParams, view, needLockTouch);
  12.         } else {
  13.             mDisposable = Observable.timer(time, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Long>() {
  14.                 @Override
  15.                 public void accept(Long aLong) throws Exception {
  16.                     createNarrowWindow(layoutParams, view, needLockTouch);
  17.                 }
  18.             });
  19.         }
  20.     }
  21. }
  22. private void createNarrowWindow(WindowManager.LayoutParams layoutParams, View view, boolean needLockTouch) {
  23.     mWindowsFloatLayout = new FloatFrameLayout(mContext);//创建一个 FloatFrameLayout,用于承载悬浮窗的内容
  24.     mWindowsFloatLayout.setLock(needLockTouch);//是否能点击
  25.     makeParm(mWindowsFloatLayout, layoutParams, view);//调整悬浮窗的布局参数(如宽度、高度等)
  26.     mWindowsFloatLayout.setView(view, 0);//将导出的 View 添加到 FloatFrameLayout 中
  27.     mWindowsFloatLayout.setWmParams(layoutParams);//将布局参数应用到悬浮窗
  28.     mWindowManager.addView(mWindowsFloatLayout, layoutParams);//将 FloatFrameLayout 添加到屏幕上,显示悬浮窗
  29.     mWindowsFloatLayout.setOnNoTouchClickListner(new FloatFrameLayout.OnNoTouchClickListner() {//
  30.         @Override
  31.         public void click(View view) {//点击悬浮窗(而不是拖动),会触发 FloatFrameLayout 的 OnNoTouchClickListner
  32.             restoreVideoFromWindowFlat(view);
  33.             //将悬浮窗中的 View 重新添加到 LiveAudienceActivity 的布局中,恢复全屏直播窗口
  34.         }
  35.         @Override
  36.         public void close(View view) {
  37.             onBackAndFinish();
  38.         }//悬浮窗右上角有一个关闭按钮,关闭悬浮窗并退出直播
  39.     });
  40. }
  41. /** 设置参数layoutParams(可能是调整?) */
  42. private void makeParm(FloatFrameLayout windowsFloatLayout, WindowManager.LayoutParams layoutParams, View view) {
  43.     Utils.initFloatParamList(this);
  44.     layoutParams.width = Utils.subWidth != 0 ? Utils.subWidth : view.getWidth();
  45.     layoutParams.height = Utils.subHeight != 0 ? Utils.subHeight + DpUtil.dp2px(20) : view.getHeight();
  46. }
  47. /*恢复界面控件到activity界面*/
  48. private void restoreVideoFromWindowFlat(View view) {
  49.     if (mWindowManager == null || !isFloatAtWindow) {//只有windowManger 不为空,并且isFloatAtWindow为true才不会被return
  50.         return;
  51.     }
  52.     try {
  53.         isFloatAtWindow = false;//清理悬浮窗的状态
  54.         mWindowManager.removeView(view);//移除悬浮窗
  55.         /*从前台点击*/
  56.         if (!ActivityMannger.getInstance().isBackGround()) {//检查当前应用是否在前台
  57.             //将主栈顶的活动(LiveAudienceActivity)推到前台
  58.             ActivityMannger.getInstance().launchOntherStackToTopActivity(false, ActivityMannger.getInstance().getMainStackTopActivity());
  59.         }
  60.         if (!isDestroyed()) {//如果活动未被销毁
  61.             restoreFlowView((FloatFrameLayout) view);//将悬浮窗中的视图重新添加到 mPlayContainer 中,恢复全屏直播窗口
  62.         }
  63.     } catch (Exception e) {
  64.         e.printStackTrace();
  65.     }
  66. }
复制代码
4. 悬浮窗的交互

悬浮窗的交互主要包括拖动和点击:
4.1 拖动悬浮窗



  • FloatFrameLayout
 的 onTouchEvent 方法监听用户的触摸变乱。




    • 当用户按下悬浮窗时,记录触摸的起始位置(mTouchStartX 和 mTouchStartY)。
    • 当用户移动手指时,调用 updateViewPosition 方法,更新悬浮窗的位置。
    • 当用户松开手指时,判定是否为点击变乱(移动间隔小于 20px),如果是点击变乱,则恢复全屏直播窗口。

4.2 点击悬浮窗



  • 如果用户点击悬浮窗(而不是拖动),会触发 FloatFrameLayout 的 OnNoTouchClickListner。

    • 调用 restoreVideoFromWindowFlat 方法,将悬浮窗中的 View 重新添加到 LiveAudienceActivity 的布局中,恢复全屏直播窗口。
    • 移除悬浮窗。

4.3 关闭悬浮窗



  • 悬浮窗右上角有一个关闭按钮,点击后会触发 FloatFrameLayout 的 onClick 方法。

    • 调用 OnNoTouchClickListner 的 close 方法,关闭悬浮窗并退出直播。


5. 悬浮窗的关闭与清理



  • restoreVideoFromWindowFlat 方法用于恢复全屏直播窗口:

    • 将悬浮窗中的 View 重新添加到 LiveAudienceActivity 的布局中。
    • 通过WindowManager.removeView 移除悬浮窗。
    • 清理悬浮窗的状态(如 isFloatAtWindow 设置为 false)。

  • clearFloatWindowState
 方法用于清理悬浮窗的状态:




    • 如果悬浮窗仍然体现,则逼迫移除悬浮窗。
    • 清理mWindowsFloatLayout 和 mWindowManager 的引用。


6. 悬浮窗的生命周期管理



  • onResume

    • 当 LiveAudienceActivity 恢复时,调用 initReceiver 初始化 HomeWatcherReceiver,监听 Home 键变乱。

  • onPause和 onStop:

    • 当 LiveAudienceActivity 进入后台时,悬浮窗会主动弹出。

  • onDestroy

    • 当 LiveAudienceActivity 销毁时,调用 clearFloatWindowState 清理悬浮窗状态。


7. 悬浮窗的权限管理



  • WindowAddHelper
 负责悬浮窗的权限管理:




    • isDrawOverLay

:检查当前是否具有悬浮窗权限。




    • requestOverLay

:跳转到系统设置页面,哀求悬浮窗权限。




    • openMakeWindowsPermissonDialog

:弹出对话框,提示用户去设置中开启权限。

8. 悬浮窗的流程图

以下是悬浮窗功能的简化流程图:

  • 触发悬浮窗




    • 用户按下 Home 键或主动触发悬浮窗。


  • 检查权限




    • 调用 checkPermissonOpenNarrow 检查悬浮窗权限。
    • 如果权限未授予,弹出对话框提示用户去设置中开启权限。


  • 体现悬浮窗




    • 调用 openNarrow,初始化悬浮窗参数并体现悬浮窗。


  • 悬浮窗交互




    • 用户拖动悬浮窗或点击悬浮窗。
    • 点击悬浮窗恢复全屏直播窗口,点击关闭按钮退出直播。


  • 清理悬浮窗




    • 调用restoreVideoFromWindowFlat 或 clearFloatWindowState 清理悬浮窗状态。



购物车ShopCartActivity


ShopCartActivity部分



  • 基础页面布局附加
  1. @Override
  2. public void init() {
  3.     setTabTitle(R.string.shop_cart);
  4.     mShopCartModel= ViewModelProviders.of(this).get(ShopCartModel.class);
  5.     mBtnMannger = (TextView) findViewById(R.id.btn_mannger);//编辑(管理)按钮
  6.     mTvGoodsNum = (TextView) findViewById(R.id.tv_goods_num);//商品数量
  7.     mRefreshView = (RxRefreshView) findViewById(R.id.refreshView);//购物车列表
  8.     mVpBottom =  findViewById(R.id.vp_bottom);//底部栏
  9.     mCheckTotalImage = (CheckImageView) findViewById(R.id.check_total_image);//全选按钮
  10.     mVpToolManger =  findViewById(R.id.vp_tool_manger);//编辑选项栏(收藏、删除)
  11.     mBtnCollect = (TextView) findViewById(R.id.btn_collect);//收藏
  12.     mBtnDelete = (TextView) findViewById(R.id.btn_delete);//删除按钮
  13.     mVpToolBuy =  findViewById(R.id.vp_tool_buy);//购买按钮
  14.     mTvTotalPrice = (TextView) findViewById(R.id.tv_total_price);//显示总价
  15.     mBtnCommit = (TextView) findViewById(R.id.btn_commit);//提交订单按钮
  16.     mTvTotalNum = (TextView) findViewById(R.id.tv_total_num);//显示商品总数
  17.     mRefreshView.setLoadMoreEnable(false);
  18.     mRefreshView.setHasFixedSize(true);
  19.     HotGoodsEmptyViewProxy hotGoodsEmptyViewProxy=new HotGoodsEmptyViewProxy();//用于配置和管理空视图
  20.     hotGoodsEmptyViewProxy.setEmptyIconId(R.drawable.bg_empty_no_cart);//设置空视图的图片
  21.     mRefreshView.setEmptyViewProxy(getViewProxyMannger(),hotGoodsEmptyViewProxy);//设置空视图代理,因为这个方法是setEmptyView空view
复制代码


  • 获取数据到适配器、空数据处置惩罚
  1. mShopCartAdapter=new ShopCartAdapter(null,getViewProxyMannger(),this,mShopCartModel);
  2. mRefreshView.setAdapter(mShopCartAdapter);
  3. mRefreshView.setRefreshEnable(false);
  4. mRefreshView.setReclyViewSetting(RxRefreshView.ReclyViewSetting.createLinearSetting(this,0));
  5. mRefreshView.setDataListner(new RxRefreshView.DataListner<MultiItemEntity>() {
  6.     @Override
  7.     public Observable<List<MultiItemEntity>> loadData(int p) {//mRefreshView获取数据
  8.         //在RxRefreshView中的抽象方法loadData() 方法的具体实现是在 ShopCartActivity 中提供的,它调用了 getData 方法来获取数据
  9.         //RefreshView中的refresh()或者loadMore()会使用到loadData获取到的数据Observable<List<MultiItemEntity>>
  10.         //在订阅的时候会调用shopCartAdapter的setData方法,把数据绑定到视图中
  11.         return getData();
  12.     }
  13.     @Override
  14.     public void compelete(List<MultiItemEntity> data) {//获取数据完成时,把数据绑定到视图
  15.         //当 mRefreshView 完成数据加载并调用 compelete(List<MultiItemEntity> data) 方法时
  16.         // List<MultiItemEntity> 会被传递给 ShopCartAdapter
  17.         mShopCartAdapter.expandAll();
  18.         if(ListUtil.haveData(data)){
  19.            ViewUtil.setVisibility(mVpBottom,View.VISIBLE);//如果有商品数据,就显示底部栏(立即下单)
  20.         }else{
  21.            ViewUtil.setVisibility(mVpBottom,View.GONE);//如果没有商品数据,就隐藏底部栏
  22.         }
  23.          notifyAllSelectButton();
  24.     }
  25.    
  26.     /*网络请求,api获取数据*/
  27. private Observable<List<MultiItemEntity>> getData() {
  28.     return ShopAPI.getShopCartData().map(new Function<ShopcartParseBean, List<MultiItemEntity>>() {
  29.         @Override
  30.         public List<MultiItemEntity> apply(ShopcartParseBean shopcartParseBean) throws Exception {
  31.             mShopcartParseBean=shopcartParseBean;
  32.             List<MultiItemEntity>list=ShopCartModel.transFormListData(shopcartParseBean);
  33.             if(mShopCartModel!=null){
  34.                mShopCartModel.setShopCartData(shopcartParseBean.getValid());// Valid有效商品列表给VM
  35.                //将有效的商品数据设置到 ShopCartModel 中,以便后续操作(如计算总价等)
  36.             }
  37.             return list;
  38.         }
  39.     }).compose(this.<List<MultiItemEntity>>bindUntilOnDestoryEvent());
  40. }
复制代码


  • 页面按键点击变乱,处置惩罚页面整体变乱(一个按键一个变乱,当用不到的时间就隐藏)
  1. /**
  2. * 其实每一个按键都是有单独的点击事件,没有变换按键的逻辑。只不过点击管理的时候变换或隐藏了布局
  3. * 让有些按键不能点击了。
  4. * 设置按键布局的显示和隐藏是一种优化,避免了一个按键的不同情况不同事件的逻辑。
  5. * @param v
  6. */
  7. @Override
  8. public void onClick(View v) {
  9.     if(!ClickUtil.canClick()){
  10.         return;
  11.     }
  12.     int id=v.getId();
  13.     if(id==R.id.btn_mannger){
  14.         judgeState();//切换购物车的编辑状态
  15.     }else if(id==R.id.btn_collect){
  16.         collect();//收藏
  17.     }else if(id==R.id.btn_delete){
  18.        deleteGoods();//删除选中的商品
  19.     }else if(id==R.id.check_total_image){//全选框
  20.         judgeAllSelect();//判断是否全选所有商品,并更新复选框状态
  21.     }else if(id==R.id.btn_commit){
  22.         commit();//提交选中的商品,进入订单确认页面
  23.     }
  24. }
  25. /**
  26.   * 提交选中的商品,进入订单确认页面
  27.   */
  28. private void commit() {
  29.      if(mShopCartModel==null){
  30.          return;
  31.      }
  32.      String[] selectId=mShopCartModel.getAllSelectCartId();//获取选中的id
  33.      if(selectId==null||selectId.length<=0){
  34.          ToastUtil.show(getString(R.string.select_goods_tip));
  35.          return;
  36.      }
  37.      String idArray=StringUtil.splitJoint(selectId);
  38.      CommitOrderActivity.forward(this,idArray);
  39. }
  40. private void judgeAllSelect() {
  41.      if(mCheckTotalImage==null){
  42.          return;
  43.      }
  44.      final boolean isTargetCheck=!mCheckTotalImage.isChecked();//判断下一次点击是选中还是取消(如果本来选中了,下一次点击就是取消)
  45.      mShopCartModel.setAllSelected(isTargetCheck);//如果原本没有选中,就让商品全部选中。如果原本有选中,商品就全部取消选中
  46.      mCheckTotalImage.setChecked(isTargetCheck);//设置选中状态
  47. }
  48. /*删除商品*/
  49. private void deleteGoods() {
  50.    final String[]allSelectId=mShopCartModel.getAllSelectCartId();//还是获取选中id
  51.      if(allSelectId==null||allSelectId.length<=0) {
  52.          ToastUtil.show(R.string.select_goods_tip);
  53.          return;
  54.      }
  55.      DialogUitl.showSimpleDialog(this, "是否要删除商品?", new DialogUitl.SimpleCallback() {
  56.          @Override
  57.          public void onConfirmClick(Dialog dialog, String content) {
  58.              if(mShopCartModel!=null){
  59.                  mShopCartModel.deleteGoodsArray(allSelectId,ShopCartActivity.this);
  60.              }
  61.          }
  62.      });
  63. }
  64. /*批量收藏商品*/
  65. private void collect() {
  66.    String[] allSelectId=getAllSelectGoodsId();//获取选中的商品id
  67.     if(allSelectId==null||allSelectId.length<=0) {
  68.         ToastUtil.show(getString(R.string.select_goods_tip));//如果没有商品被选中就提示
  69.         return;
  70.     }
  71.     ShopAPI.batchCollect(allSelectId, ShopState.PRODUCT_DEFAULT).//api接入收藏商品
  72.             compose(this.<Boolean>bindUntilOnDestoryEvent())
  73.             .subscribe(new DefaultObserver<Boolean>() {
  74.                 @Override
  75.                 public void onNext(Boolean aBoolean) {
  76.                     if(aBoolean){
  77.                       ToastUtil.show(R.string.collect_succ);
  78.                     }
  79.                 }
  80.             });
  81. }
  82. private String[] getAllSelectGoodsId() {//封装获取所有选中的商品id
  83.      if(mShopCartModel==null){
  84.          return null;
  85.      }
  86.      String[] allSelectId=mShopCartModel.getAllSelectGoodsId();
  87.      return allSelectId;
  88. }
复制代码

适配器ShopCartAdapter

适配器是设置RxRefreshView列表的列表项的
  1. public class ShopCartAdapter <T extends MultiItemEntity> extends BaseMutiRecyclerAdapter<T, BaseReclyViewHolder>{
  2.     //ShopCartAdapter--BaseMutiRecyclerAdapter--BaseMultiItemQuickAdapter--BaseQuickAdapter
  3.     //只要适配器继承了 BaseQuickAdapter,然后在 convert() 里用 addOnClickListener(viewId) 绑定子视图
  4.     // 就可以用 setOnItemChildClickListener() 监听它,而不会触发 setOnItemClickListener()
  5.     // 因为 BaseQuickAdapter 已经封装好了事件分发逻辑
  6.     private BaseProxyMannger mBaseProxyMannger;
  7.     private Context mContext;
  8.     private ShopCartModel mShopCartModel;
  9.     /**
  10.      * @param data
  11.      * @param baseProxyMannger
  12.      * 对于复杂的、封装过的功能组件(如 ShopCartGoodsNumViewProxy),可以使用 BaseProxyMannger 来管理和复用这些组件。
  13.      * 这种方式适用于需要频繁创建和销毁的视图组件,或者具有复杂交互逻辑的组件。通过 BaseProxyMannger,可以更好地管理和复用这些组件,简化事件监听器的绑定,并提高性能。
  14.      * @param context
  15.      * @param shopCartModel
  16.      */
  17.     public ShopCartAdapter(List<T> data,BaseProxyMannger baseProxyMannger,Context context,ShopCartModel shopCartModel) {
  18.         super(data);
  19.         mContext=context;
  20.         mBaseProxyMannger=baseProxyMannger;//一个视图代理管理器,用于动态管理和复用视图组件(如商品数量选择器)
  21.         //其实只要记住:BaseProxyMannger在商品item中用来管理商品数量选择器就行了,不太需要过多了解
  22.         mShopCartModel=shopCartModel;
  23. /*过期状态的layout*/
  24. addItemType(ShopCartStoreBean.TYPE_INVALID, R.layout.item_recly_shop_cart_invalid_title);
  25. addItemType(ShopCartBean.TYPE_INVALID,R.layout.item_relcy_shop_cart_invaild_goods);
  26. /*正常状态的layout*/
  27. addItemType(ShopCartBean.TYPE_VALID,R.layout.item_relcy_shop_cart_goods);//商品item
  28. addItemType(ShopCartStoreBean.TYPE_VALID,R.layout.item_recly_shop_cart_store);//店铺item,点到就全选
复制代码


  • 设置商品和商品选项框的点击变乱
  1. setOnItemClickListener(new OnItemClickListener() {//监听每个商品item点击事件(选项框除外)
  2.     @Override
  3.     public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
  4.         MultiItemEntity multiItemEntity=getItem(position);
  5.         if(multiItemEntity==null){
  6.             return;
  7.         }
  8.         int itemType=multiItemEntity.getItemType();
  9.         if(multiItemEntity instanceof ShopCartStoreBean){
  10.             if(itemType==ShopCartStoreBean.TYPE_INVALID){//如果点到失效商品
  11.                clickInVaildHead(multiItemEntity,position);//点击失效商品进行开关
  12.             }else{
  13.                clickStore(multiItemEntity,view,position);//点击店铺
  14.             }
  15.         }else if(multiItemEntity instanceof ShopCartBean){
  16.             if(itemType==ShopCartBean.TYPE_VALID){
  17.                clickGoods(multiItemEntity,view,position);//点击商品跳转到商品详情页
  18.             }
  19.         }
  20.     }
  21. });
  22. setOnItemChildClickListener(new OnItemChildClickListener() {//监听选项框的点击事件
  23.     @Override
  24.     public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
  25.         MultiItemEntity multiItemEntity=getItem(position);
  26.         if(multiItemEntity==null){
  27.             return;
  28.         }
  29.         int type=multiItemEntity.getItemType();
  30.         if(type==ShopCartStoreBean.TYPE_VALID){
  31.             clickStore(multiItemEntity,view,position);//全选店铺对应商品
  32.         }else if(type==ShopCartBean.TYPE_VALID){
  33.             clickCheckGoods(multiItemEntity,view,position);//点击切换商品选中状态
  34.         }else if(type==ShopCartStoreBean.TYPE_INVALID){
  35.             deleteAllInVidGood();
  36.         }
  37.     }
  38. });
复制代码
setOnItemClickListener和setOnItemChildClickListener是用到了BaseQuickAdapter,可以给列表项(这里是商品项)和列表项的子项(商品的选项框)分别实现不同的点击变乱
只需要在convert方法中标记ItemChild,就可以使用setOnItemChildClickListener为子项设置setOnItemChildClickListener
  1. helper.addOnClickListener(R.id.check_image);// 标记 CheckImageView 为 ItemChild
复制代码


  • 适配器的convert绑定数据item和视图BaseReclyViewHolder
  1. /*过期状态的layout*/
  2. addItemType(ShopCartStoreBean.TYPE_INVALID, R.layout.item_recly_shop_cart_invalid_title);
  3. addItemType(ShopCartBean.TYPE_INVALID,R.layout.item_relcy_shop_cart_invaild_goods);
  4. /*正常状态的layout*/
  5. addItemType(ShopCartBean.TYPE_VALID,R.layout.item_relcy_shop_cart_goods);//商品item
  6. addItemType(ShopCartStoreBean.TYPE_VALID,R.layout.item_recly_shop_cart_store);//店铺item,点到就全选
  7. /**
  8. * 对于简单的、基础的视图组件(如 TextView、ImageView、CheckImageView 等),
  9. * 可以直接在 convert 方法中通过 BaseReclyViewHolder 提供的方法进行绑定和更新。
  10. * 这种方式简洁明了,适合处理较为简单的视图状态变化。
  11. * 简单直接:代码逻辑清晰,易于理解和维护。 性能高效:避免了不必要的复杂操作,提高了性能。
  12. * @param helper A fully initialized helper.
  13. * @param item   The item that needs to be displayed.
  14. */
  15. @Override
  16. protected void convert(@NonNull BaseReclyViewHolder helper, T item) {//在convert中要用helper.setXX绑定视图
  17.     //每个 convertXXX 方法负责将具体的 ShopCartStoreBean 或 ShopCartBean
  18.     //在 ShopCartAdapter 中,convert 方法根据不同的 itemType 调用不同的转换方法,以处理不同类型的数据项,确保代码结构清晰且易于维护。
  19.     //上面使用addItemType设置不同数据项的布局,在这里可以根据不同类型数据项的不同布局来绑定对应的数据。区分地绑定数据和布局
  20.     switch (item.getItemType()){
  21.         case ShopCartStoreBean.TYPE_INVALID:
  22.             convertStoreInvalid(helper,item);
  23.             break;
  24.         case ShopCartStoreBean.TYPE_VALID:
  25.             convertStoreValid(helper,item);
  26.             break;
  27.         case ShopCartBean.TYPE_INVALID:
  28.             convertGoodsInvalid(helper,item);
  29.             break;
  30.         case ShopCartBean.TYPE_VALID:
  31.             convertGoodsValid(helper,item);
  32.             break;
  33.         default:
  34.             break;
  35.     }
  36. }
  37. /*正常商品的店铺*/
  38. private void convertStoreValid(BaseReclyViewHolder helper, T item) {
  39.     ShopCartStoreBean shopCartStoreBean= (ShopCartStoreBean) item;
  40.     helper.setText(R.id.tv_name,shopCartStoreBean.getName());//绑定店铺名称
  41.     CheckImageView checkImageView=helper.getView(R.id.check_image);
  42.     checkImageView.setChecked(shopCartStoreBean.isChecked());//绑定选定状态
  43. }
  44. /*正常商品展示*/
  45. private void convertGoodsValid(BaseReclyViewHolder helper, T item) {
  46.     ShopCartBean shopCartBean= (ShopCartBean) item;
  47.     GoodsBean goodsBean=shopCartBean.getProductInfo();
  48.     View container=helper.getView(R.id.container);
  49.     if(goodsBean!=null){
  50.         helper.setText(R.id.tv_title,goodsBean.getName());//商品标题
  51.         helper.setText(R.id.tv_price,goodsBean.getUnitPrice());//商品价格
  52.         helper.setImageUrl(goodsBean.getThumb(),R.id.img_thumb);//商品图片
  53.         SpecsValueBean specsValueBean=goodsBean.getAttrInfo();
  54.         if(specsValueBean!=null){
  55.           helper.setText(R.id.tv_field, WordUtil.getString(R.string.goods_field_tip,specsValueBean.getSuk()));
  56.         }
  57.     }
  58.     CheckImageView checkImageView=helper.getView(R.id.check_image);
  59.     checkImageView.setChecked(shopCartBean.isChecked());
  60.     ViewGroup nameContainer=helper.getView(R.id.vp_number_container);// 商品数量容器(+/-)
  61.     helper.addOnClickListener(R.id.check_image);// 标记 CheckImageView 为 ItemChild
  62.     if(mBaseProxyMannger==null||container==null){
  63.         return;
  64.     }
  65.     ShopCartGoodsNumViewProxy goodsNumViewProxy=null;
  66.     String tag=Integer.toString(container.hashCode());
  67.     BaseViewProxy baseViewProxy=mBaseProxyMannger.getViewProxyByTag(tag);//检查是否存在与指定 tag 对应的视图代理
  68.     if(baseViewProxy!=null){
  69.         goodsNumViewProxy= (ShopCartGoodsNumViewProxy)baseViewProxy ;
  70.     }else{
  71.         goodsNumViewProxy=new ShopCartGoodsNumViewProxy();
  72.         goodsNumViewProxy.setEableDelay(true);
  73.         goodsNumViewProxy.setNumberChangeListnter(mNumberChangeListnter);
  74.         mBaseProxyMannger.addViewProxy(nameContainer,goodsNumViewProxy,tag);//把商品数量容器布局跟商品item里的视图vp_number_container绑定
  75.         //这里只需要知道商品数量选择器的功能视图goodsNumViewProxy跟商品item的布局vp_number_container绑定就行
  76.     }
  77.        goodsNumViewProxy.setShopCartBean(shopCartBean);
  78. }
  79. /*过期商品展示*/
  80. private void convertGoodsInvalid(BaseReclyViewHolder helper, T item) {
  81.     ShopCartBean shopCartBean= (ShopCartBean) item;
  82.     GoodsBean goodsBean=shopCartBean.getProductInfo();
  83.     if(goodsBean!=null){
  84.         helper.setText(R.id.tv_title,goodsBean.getName());
  85.         //helper.setText(R.id.tv_price,goodsBean.getUnitPrice());
  86.         helper.setImageUrl(goodsBean.getThumb(),R.id.img_thumb);
  87.     }
  88. }
  89. /**  监听商品数字变化  */
  90. GoodsNumViewProxy.NumberChangeListnter<ShopCartGoodsNumViewProxy> mNumberChangeListnter= new GoodsNumViewProxy.NumberChangeListnter<ShopCartGoodsNumViewProxy> () {
  91.     @Override
  92.     public void change(ShopCartGoodsNumViewProxy goodsNumViewProxy, int num) {
  93.        if(mShopCartModel!=null&&goodsNumViewProxy.haveData()){
  94.           ShopCartBean shopCartBean=goodsNumViewProxy.getShopCartBean();
  95.           mShopCartModel.modifyCartNum(shopCartBean,num);//更新数据
  96.        }
  97.     }
  98. };
  99. /*过期商品统统归类为过期商品店铺*/
  100. private void convertStoreInvalid(BaseReclyViewHolder helper, T item) {
  101.     final ShopCartStoreBean level0Bean= (ShopCartStoreBean) item;
  102.     View arrowView=helper.getView(R.id.img_arrow);
  103.     if(level0Bean.isExpanded()){
  104.         arrowView.setRotation(180);
  105.     }else{
  106.         arrowView.setRotation(0);
  107.     }
  108.     helper.addOnClickListener(R.id.btn_delete);
  109. }
复制代码









免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

老婆出轨

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表