老婆出轨 发表于 昨天 04:29

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

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

https://i-blog.csdnimg.cn/direct/990986354aa947eb8839c1697a022fee.png
private FrameLayout mVpTopContainer;//私信搜索购物车
private FrameLayout mVpBannerContainer;//轮播图容器
private MagicIndicator mIndicator;//标签栏
private ViewPager mViewPager;//直播间item
private BannerViewProxy<BannerBean>mBannerViewProxy;//轮播图 轮播图计划

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


[*]setClipToOutline(true) 开启控件裁剪。
[*]ViewOutlineProvider 自定义表面,实现圆角矩形效果,setRoundRect(0, 0, ...) 控制裁剪地区,radius 为圆角大小。
轮播图指示器
https://i-blog.csdnimg.cn/direct/66e73ed6b98d46678c8ea95ce77a0752.png
private IndicatorView defaultIndicator() {
    IndicatorView indicator = new IndicatorView(getActivity())
            .setIndicatorColor(ResourceUtil.getColor(getActivity(),R.color.alpha_white_3f))// 普通指示器颜色
            .setIndicatorSelectorColor(Color.WHITE)// 选中指示器颜色
            .setIndicatorRatio(5f) //ratio,默认值是1 ,也就是说默认是圆点,根据这个值,值越大,拉伸越长,就成了矩形,小于1,就变扁了呗
            .setIndicatorRadius(2f) // radius 点的大小
            .setIndicatorSelectedRatio(5f)// 选中状态宽高比
            .setIndicatorSelectedRadius(2f)// 选中状态圆角半径
            .setIndicatorStyle(IndicatorView.IndicatorStyle.INDICATOR_BIG_CIRCLE);
    return indicator;
}
轮播图下面的小圆点指示器,他把它改成扁横线了


[*]颜色

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



[*]形状

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



[*]样式

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

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

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

https://i-blog.csdnimg.cn/direct/5f75ba4b58534d54a3d0b6bff8e15d99.png
private MagicIndicator mIndicator;//标签栏
List<LiveClassBean>liveClassBean=featureBean.getLiveclass();//获取直播分类标签
initIndicator(liveClassBean);//设置每个标签的对应直播间列表 先拿到对应标签栏列表liveClassBean
private void initIndicator(List<LiveClassBean>liveClassBeanList) {
    if(mIndicatorList!=null||mViewPager.getChildCount()>1){//如果页面已经初始化了就返回,避免重复初始化
      return;
    }
    if(mIndicatorList==null){//因为一开始标签列表就是空的,所以先创建关注和精选
       mIndicatorList=new ArrayList<>();
       LiveClassBean liveClassBean=new LiveClassBean();
       liveClassBean.setId(LiveClassBean.FOLLOW);//关注
       liveClassBean.setName(getString(R.string.follow));
       mIndicatorList.add(liveClassBean);

       liveClassBean=new LiveClassBean();
       liveClassBean.setId(LiveClassBean.FEATURED);//精选
       liveClassBean.setName(getString(R.string.featured));
       mIndicatorList.add(liveClassBean);
    }
    if(liveClassBeanList!=null){//这个时候因为已经创建了关注和精选了,所以又不是空的,直接把网络获取的标签列表都加进去
       mIndicatorList.addAll(liveClassBeanList);//就导致多添加了一次关注和精选
    }
    List<HomeLiveViewProxy> viewProxyList=initLiveViewList();//基础标签栏列表加载完成后执行方法
    ViewProxyPageAdapter pageAdapter = new ViewProxyPageAdapter(getViewProxyChildMannger(),viewProxyList);
    mViewPager.setOffscreenPageLimit(viewProxyList.size());//设置 ViewPager 的 offscreenPageLimit 为 viewProxyList 的大小。这意味着 ViewPager 会预加载所有页面,避免在切换页面时出现空白或加载延迟。
    CommonNavigator commonNavigator = new CommonNavigator(getActivity());//MagicIndicator的一种标签栏类型(总框架,包括宽度、对齐方式、标签的动画效果,标签大小,指示器样式啥的)
    MainNavigatorAadpter mainNavigatorAadpter=new MainNavigatorAadpter(mIndicatorList,getActivity(),mViewPager);//MainNavigatorAadpter(自定义的适配器继承CommonNavigatorAdapter,用于控制标签栏的显示样式和点击事件,确保 MagicIndicator 的标签内容与 ViewPager 页面内容保持同步。)
    mainNavigatorAadpter.setEableScale(false);//设置标签栏不启用缩放效果,即标签选中时不会缩放。
    commonNavigator.setAdapter(mainNavigatorAadpter);//设置标签栏的适配器为mainNavigatorAadpter
    mIndicator.setNavigator(commonNavigator);//MagicIndicator 提供了多种类型的 Navigator(比如 CommonNavigator、LineNavigator、CircleNavigator 等),而 CommonNavigator 只是其中的一种,所以需要MagicIndicator来进一步封装
    ViewPagerHelper.bind(mIndicator, mViewPager);//当滑动不同页面时,ViewPagerHelper更新标签
    pageAdapter.attachViewPager(mViewPager,1);
} 留意initIndicator()方法执行到
List<HomeLiveViewProxy> viewProxyList=initLiveViewList();//基础标签栏列表加载完成后执行方法 initLiveViewList()的时间,initLiveViewList()就是把mIndicatorList每个标签对应的直播间列表生成出来(根据标签id调用api获取对应的直播列表,每个列表元素是HomeLiveViewProxy,通过调用适配器HomeLiveAdapter,可以把api获取到的数据跟每个直播间item视图绑定起来)。api获取到每个标签栏直播间列表的数据就是一个HomeLiveViewProxy
private RxRefreshView<LiveBean> mRefreshView;//一个支持刷新加载的 UI 组件(能刷新的容器)
private HomeLiveAdapter mHomeLiveAdapter;//每个直播间item适配器,给每个LiveBean元素都适配成一个直播间item 每个HomeLiveViewProxy就是一个页面,但是不是viewpaper,以是要把它转成viewpaper然后跟MagicIndicator联动
把他转成mViewPager。应该说是把HomeLiveViewProxy内容都附加到mViewPager上。而把每页数据附加到一个多页控件上就需要适配器。
ViewProxyPageAdapter pageAdapter = new ViewProxyPageAdapter(getViewProxyChildMannger(),viewProxyList);// ViewPager 不直接知道怎么管理这些 HomeLiveViewProxy 页面。所以我们用 ViewProxyPageAdapter 这个“中介”告诉 ViewPager: 获取到每个标签的HomeLiveViewProxy再给每个HomeLiveViewProxy加一个检查器(直播间列表的直播间检查presenter)
private LiveRoomCheckLivePresenter mCheckLivePresenter;
if(mCheckLivePresenter==null){
   mCheckLivePresenter=new LiveRoomCheckLivePresenter(getActivity(),this);
}
for(HomeLiveViewProxy viewProxy:list){
    viewProxy.setCheckLivePresenter(mCheckLivePresenter);//每个标签设置一个直播间列表的直播间检查presenter
}
加入变成列表list,返回list
private List<HomeLiveViewProxy> initLiveViewList() {
RecyclerView.RecycledViewPool recycledViewPool=new RecyclerView.RecycledViewPool();
List<HomeLiveViewProxy>list=new ArrayList<>();
for(final LiveClassBean liveClassBean:mIndicatorList){//对每个标签栏创建对应的直播间列表
       int id=liveClassBean.getId();//根据每个标签栏的id来创建对应的标签栏对应的直播间列表
      HomeLiveViewProxy homeLiveViewProxy=null;
       if(id==LiveClassBean.FEATURED){//如果是精选
       homeLiveViewProxy=new HomeLiveViewProxy() {
             @Override
             public Observable<List<LiveBean>> getData(int p) {
               return MainAPI.getFeatured(p).map(new Function<FeatureBean, List<LiveBean>>() {
                     @Override
                     public List<LiveBean> apply(FeatureBean featureBean) throws Exception {
                         return featureBean.getList();
                     }
               });
             }
         };
       }else if(id==LiveClassBean.FOLLOW){//如果是关注
         homeLiveViewProxy=new HomeLiveViewProxy() {
               @Override
               public Observable<List<LiveBean>> getData(int p) {
                   return MainAPI.getLiveListByFollow(p);
               }
         };
       }else{//如果是其他标签,则根据标签ID获取对应直播间列表
         homeLiveViewProxy=new HomeLiveViewProxy(){
               @Override
               public Observable<List<LiveBean>> getData(int p) {
                return MainAPI.getLiveListByClass(liveClassBean.getId(),p);//根据标签ID获取对应直播间列表
               }
         };
       }

      homeLiveViewProxy.setRecycledViewPool(recycledViewPool);// 为 homeLiveViewProxy 设置复用池 recycledViewPool(recycledViewPool用于缓存和复用 RecyclerView.ViewHolder)
      list.add(homeLiveViewProxy);//当有多个 RecyclerView(如多个页面的列表)时,它们可以共享 recycledViewPool,避免频繁创建和销毁 ViewHolder
}
    if(mCheckLivePresenter==null){
       mCheckLivePresenter=new LiveRoomCheckLivePresenter(getActivity(),this);
    }
    for(HomeLiveViewProxy viewProxy:list){
      viewProxy.setCheckLivePresenter(mCheckLivePresenter);//每个标签设置一个直播间列表的直播间检查presenter
    }

    return list;
} 最核心的9行代码 
    ViewProxyPageAdapter pageAdapter = new ViewProxyPageAdapter(getViewProxyChildMannger(),viewProxyList);
    mViewPager.setOffscreenPageLimit(viewProxyList.size());//设置 ViewPager 的 offscreenPageLimit 为 viewProxyList 的大小。这意味着 ViewPager 会预加载所有页面,避免在切换页面时出现空白或加载延迟。
    CommonNavigator commonNavigator = new CommonNavigator(getActivity());//MagicIndicator的一种标签栏类型(总框架,包括宽度、对齐方式、标签的动画效果,标签大小,指示器样式啥的)
    MainNavigatorAadpter mainNavigatorAadpter=new MainNavigatorAadpter(mIndicatorList,getActivity(),mViewPager);//MainNavigatorAadpter(自定义的适配器继承CommonNavigatorAdapter,用于控制标签栏的显示样式和点击事件,确保 MagicIndicator 的标签内容与 ViewPager 页面内容保持同步。)
    mainNavigatorAadpter.setEableScale(false);//设置标签栏不启用缩放效果,即标签选中时不会缩放。
    commonNavigator.setAdapter(mainNavigatorAadpter);//设置标签栏的适配器为mainNavigatorAadpter
    mIndicator.setNavigator(commonNavigator);//MagicIndicator 提供了多种类型的 Navigator(比如 CommonNavigator、LineNavigator、CircleNavigator 等),而 CommonNavigator 只是其中的一种,所以需要MagicIndicator来进一步封装
    ViewPagerHelper.bind(mIndicator, mViewPager);//当滑动不同页面时,ViewPagerHelper更新标签
    pageAdapter.attachViewPager(mViewPager,1);//将 viewProxyList 通过 pageAdapter 附加到 ViewPager 上。attachViewPager() 将实际页面(即 HomeLiveViewProxy 实例)绑定到 ViewPager 中,让它能够真正展示页面内容。 这里是先把标签和viewpaper的框架先做好再给每个viewpaper附加对应的mViewPager页面(mHomeLiveViewProxy转换成的)
pageAdapter.attachViewPager(mViewPager,1);//将 viewProxyList 通过 pageAdapter 附加到 ViewPager 上。attachViewPager() 将实际页面(即 HomeLiveViewProxy 实例)绑定到 ViewPager 中,让它能够真正展示页面内容。 把框架做好了,末了就用适配器把viewProxyList适配给mViewPager(末了这句才是真正把内容贴上去每个viewpager,前面mViewPager还是一个空白页,只不过是有对应标签的空白页)
至于为什么是pageAdapter.attachViewPager而不是mViewPager.setAdapter,因为pageAdapter.attachViewPager已经把包括ViewPager.setAdapter的都封装好了
public void attachViewPager(@Nullable ViewPager viewPager,int position){
    L.e("attachViewPager执行了");
    mInstantiatePostion=position;
    viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
      @Override
      public void onPageScrolled(int i, float v, int i1) {
      }
      @Override
      public void onPageSelected(int i) {
            if(mCurrentBaseViewProxy!=null){
               mCurrentBaseViewProxy.setUserVisibleHint(false);
            }
               mCurrentBaseViewProxy=mViewList.get(i);
               mCurrentBaseViewProxy.setUserVisibleHint(true);
      }
      //页面切换时的逻辑
      //每次切换页面时,先将上一个页面 mCurrentBaseViewProxy 标记为不可见(setUserVisibleHint(false))。
      //然后将当前页面 mCurrentBaseViewProxy 设置为可见(setUserVisibleHint(true))。
      @Override
      public void onPageScrollStateChanged(int i) {
      }
    });
    viewPager.setAdapter(this);//这里也已经把setAdapter做了
    viewPager.setCurrentItem(position);//attachViewPager() 会直接让 ViewPager 显示到指定的初始位置 position = 1,
    //这样无需额外写 mViewPager.setCurrentItem(1);。
} mViewPager.setAdapter(pageAdapter); 仅仅做了一件事:


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

[*]页面切换逻辑的控制


[*]

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


[*]生命周期管理


[*]

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


[*]默认选中页面


[*]

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



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


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


[*]目次制作:

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



[*]活页空白纸加上去:

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



[*]写内容到纸上:

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


商品分类界面

https://i-blog.csdnimg.cn/direct/ef14d7c2f25d4be28183cf48c003fe11.png
初始布局设置

private RecyclerView mReclyviewNavigation;//左边列表视图
private RecyclerView mReclyviewClassify;//右边列表视图
private ClassifyIndexAdapter mClassifyIndexAdapter;//左侧列表适配器
private ClassifyAdapter mClassifyAdapter;//右边列表适配器
private GridLayoutManager mGridLayoutManager;//网格列表布局
private EditText mEtSearch;//搜索框

LinearLayoutManager linearLayoutManager=new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false);
mClassifyIndexAdapter=new ClassifyIndexAdapter(null);//左侧列表适配器
mReclyviewNavigation.setAdapter(mClassifyIndexAdapter);
mReclyviewNavigation.setLayoutManager(linearLayoutManager);
initSearch();
mGridLayoutManager=new GridLayoutManager(getActivity(),3);//网格布局 展示商品分类,每行显示 3 列
initClassifyReclyView();

private void initSearch() {//搜索框初始化
    mEtSearch = findViewById(R.id.et_search);
    mEtSearch.setHint(R.string.search_goods);
    ViewUtil.setEditextEnable(mEtSearch);
    mEtSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
      @Override
      public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {//当按下搜索按钮时
            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                forwardSearch(v.getText().toString());//跳转到搜索界面,传入输入的字符串
                v.clearFocus();
                return true;
            }
            return false;
      }
    });
}
/*初始化右边列表*/
private void initClassifyReclyView() {
    mReclyviewClassify.setLayoutManager(mGridLayoutManager);//把网格布局管理者放进去
    mClassifyAdapter=new ClassifyAdapter(R.layout.item_recly_section_classify_normal,R.layout.item_recly_section_classify_head,null);
    //第一个layout布局是普通项,第二个layout布局是头部项,第三个是数据列表,这里是null后面再设置
    //自定义列表适配器
    mClassifyAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {//如果点到右侧列表的商品时
      @Override
      public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
             if(mClassifyAdapter==null){
                  return;
            }
            ClassifySectionBean sectionBean=mClassifyAdapter.getItem(position);//点击时拿到商品item索引
            if(!sectionBean.isHeader&&sectionBean.t!=null){
            ClassifyBean classifyBean=sectionBean.t;
            GoodsSearchArgs goodsSearchArgs=new GoodsSearchArgs();
            goodsSearchArgs.cid=classifyBean.getPid();//把信息传到goodsSearchArgs对象
            goodsSearchArgs.sid=classifyBean.getId();
            goodsSearchArgs.className=classifyBean.getName();
            ShopSearchActivity.forward(getActivity(),goodsSearchArgs);//获取对应item信息,传递搜索参数跳转到 ShopSearchActivity
            }
      }
    }); 此中R.id.vp_search_container是继续布局,以是这个页面一开始就已经把顶部搜索框、左边列表、右边列表,基础框架做好了,之后就是往左边列表的适配器加标签栏数据,往右边的适配器加网格管理器和数据就行
留意这里的ClassifySectionBean 是extends SectionEntity<ClassifyBean>
而这个SectionEntity的泛类T是通过sectionBean.t来获取
public abstract class SectionEntity<T> implements Serializable {
    public boolean isHeader;
    public T t;
    public String header;

    public SectionEntity(boolean isHeader, String header) {
      this.isHeader = isHeader;
      this.header = header;
      this.t = null;
    }

    public SectionEntity(T t) {
      this.isHeader = false;
      this.header = null;
      this.t = t;
    }
} 左右列表

标签分类情况:每个标签栏是一个ClassifyBean,而ClassifyBean.children是一个List
在mData和info的时间:每个标签栏包含一个子商品列表,而initSectionBeanData里面的
List info是引用类型,以是mData=info实在这两个变量都是指向同一个List,以是在initSectionBeanData是给mData里的每个classifyBean都设置了标签
给标签头设置index
ClassifyBean classifyBeanParent=info.get(i);//info.get(i)是一个classifyBean对象,所以还是引用类型
classifyBeanParent.setIndex(index);//给每个标签设置索引。(给mData每个classifyBean(标签头)设置index)

给标签的每个子商品设置索引
List<ClassifyBean> childArray=classifyBeanParent.getChildren();//获取当前标签的子商品列表
for(ClassifyBean classifyBeanChild :childArray){
    classifyBeanChild.setIndex(index);//给每个子商品设置索引。因为是引用类型,所以这里setIndex之后mData的ClassifyBean也是可以getIndex
    index++;
} 在mData:标签和子商品列表是上下级关系
这里是修改了List mData,标签头是mData.get(position)。
而标签下的子商品列表是mData.get(position).getChildren()
//设置标签栏封装成classifySectionBean类,装入sectionBeanList
List<ClassifySectionBean> sectionBeanList=new ArrayList<>();
ClassifySectionBean classifySectionBean=new ClassifySectionBean(true,classifyBean.getName());
classifySectionBean.setIndex(i);//设置右侧标签头索引(注意是从0开始,且索引是i而不是index),为了对应左侧导航栏的分类标签顺序(一开始看成index了)
sectionBeanList.add(classifySectionBean);//把标签头封装成 ClassifySectionBean 对象,并添加到 sectionBeanList 中

//把子商品列表的每个商品封装成classifySectionBean类,装入sectionBeanList
for(ClassifyBean classifyBeanChild :childArray){
    classifySectionBean=new ClassifySectionBean(classifyBeanChild);//把子商品封装成 ClassifySectionBean 对象,并添加到 sectionBeanList 中
    sectionBeanList.add(classifySectionBean);
}
if(mClassifyAdapter!=null){
   mClassifyAdapter.setData(sectionBeanList);//把sectionBeanList装入右侧列表适配器
} 在这个部分由classifySectionBean封装的标签头、商品都是同一个类,就是平级关系,因为是为了右侧布局管理,每个标签头或者商品都是一个独立地区,没有包含关系。
左侧列表的List<ClassifyBean> mData的层级关系:标签栏和子商品是上下级
https://i-blog.csdnimg.cn/direct/dd1855daf28849df9f1f5f7c6d375ded.png
https://i-blog.csdnimg.cn/direct/88232813d4754ff3b9d88dd67298bcc8.png
右侧List<ClassifySectionBean> sectionBeanList 层级关系:标签头和商品是平级关系
https://i-blog.csdnimg.cn/direct/5a87756e8afd48dcb4749b0780aec3cc.png
 左侧标签栏和右侧列表的联动

左侧标签栏点击,右侧滑动到相应标签头

mClassifyIndexAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
    @Override
    public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
       classifyScrollPosition(position);//当点击左侧标签栏时调用 classifyScrollPosition() 方法滚动右侧列表。
    }
});

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

商品界面 GoodsDetailActivity

商品标签GoodsPannelViewProxy

初始化轮播图
https://i-blog.csdnimg.cn/direct/af4a09c1dbe64b1f9d3b982ac497f6a3.png
mBanner.setAutoPlay(false).setIndicator(defaultIndicator())//适配指示器
      .setAdapter(mImageAdapter);//适配器
      
mImageAdapter.setData(bannerList);//把图片数据装入适配器
商品标签大部分简朴数据直接从mStoreGoodsBean获取
https://i-blog.csdnimg.cn/direct/3c3fe729a44c412c81066816cd41f987.png
mTvPrice.setText(StringUtil.getPrice(mStoreGoodsBean.getPrice()));
mTvOtPrice.setText(getString(R.string.original_tip,mStoreGoodsBean.getOriginalPrice()));
mTvTitle.setText(mStoreGoodsBean.getName());
String uint=mStoreGoodsBean.getUnitName();
mTvStock.setText(getString(R.string.stock_tip,mStoreGoodsBean.getStock(),uint));
mTvSaleNum.setText(getString(R.string.sale_num_tip,mStoreGoodsBean.getSales(),uint)); SpecsSelectViewProxy商品规格选择

https://i-blog.csdnimg.cn/direct/bf6f5d171c654a6e8bac9d6f5dad1178.png
//规格选择难点:规格尺寸适配器
SpecSelectAdapter adapter = new SpecSelectAdapter(specsBeanList,key, getViewProxyMannger().getLayoutInflater()); https://i-blog.csdnimg.cn/direct/c3db071a760d4802b2fcd94e8a47d3de.png
初始化:获取默认选项,适配每一类的规格选择框(layoutPosition类的位置,flexRadioGroup选项的容器,labelList此类规格可选项列表)
更新变乱:
public SpecSelectAdapter(List<SpecsBean> list, String selectKey, LayoutInflater layoutInflater) {
    //为什么是List<SpecsBean>,因为有的商品规格要选的不仅一项,可能是两项以上,比如颜色和尺寸都要选,那就是两个specsBean
    super(list);
    if (selectKey != null) {
      specKeyArray = selectKey.split(",");//初始化默认规格选项
    }
    mLayoutInflater = layoutInflater;
}

@Override
public void convert(BaseReclyViewHolder helper, SpecsBean item) {
    helper.setText(R.id.tv_title, item.getName());//规程名称(颜色、码数等)
    int position = helper.getObjectPosition();//规格总布局位置(两个规格颜色和尺寸,颜色position是0,尺寸position是1)
    List<String> labelList = item.getValue();
    FlexRadioGroup flexRadioGroup = helper.getView(R.id.flex);
    //设置 FlexRadioGroup 的布局方向和换行方式。
    flexRadioGroup.setFlexWrap(FlexWrap.WRAP);
    flexRadioGroup.setFlexDirection(FlexDirection.ROW);
    initChild(position, flexRadioGroup, labelList);
}

private int getChildPosition(View view) {
    Object object = view.getTag();
    if (object != null && object instanceof Integer) {
      return (int) object;
    }
    return -1;
}

private void initChild(final int layoutPosition, final FlexRadioGroup flexRadioGroup, final List<String> labelList) {
    if (!ListUtil.haveData(labelList) || mLayoutInflater == null) {
      return;
    }

    flexRadioGroup.setOnCheckedChangeListener(new FlexRadioGroup.OnCheckedChangeListener() {
      @Override//选中项发生更改时
      public void onCheckedChanged(int checkedId) {
            //设置 FlexRadioGroup 的选中变化监听器,当选择发生变化时更新 specKeyArray 并调用 specKeyChange 方法。
            View view = flexRadioGroup.findViewById(checkedId);
            if (view == null) {
                return;
            }
            int childPosition = getChildPosition(view);
            int size = ListUtil.getSize(specKeyArray);
            if (childPosition != -1 && size > layoutPosition) {
                //更新specKeyArray当前所选规格的类中的选择项的值,比如默认[红色,XL],更改颜色规格为蓝色,则为[蓝色,XL]
                specKeyArray = labelList.get(childPosition);
                specKeyChange();
            } else {
                DebugUtil.sendException("specKeyArray大小必须大于layoutPosition");
            }
      }
    });

    int size = labelList.size();
    //获取并返回 specKeyArray 中 layoutPosition(当前选中位置) 位置的元素
    String key = ListUtil.getArrayData(specKeyArray, layoutPosition);
    for (int i = 0; i < size; i++) {
      String label = labelList.get(i);
      boolean isChecked = false;//初始状态为未点击
      if (StringUtil.equals(key, label)) {
            isChecked = true;//如果当前要设置的键与选中的键相同,则设置为点击选中状态
      }
      //遍历标签列表,创建 RadioButton 并设置其文本、标签和初始选中状态,然后添加到 FlexRadioGroup 中。
      RadioButton radioButton = (RadioButton) mLayoutInflater.inflate(R.layout.item_relcy_spec_child, flexRadioGroup, false);
      radioButton.setText(label);//设置每个可选规格按键的值
      radioButton.setTag(i);//设置标签
      flexRadioGroup.addView(radioButton);//把按键加到flexRadioGroup容器
      radioButton.setChecked(isChecked);//初始状态
    }
} 评价标签GoodsEvaluateViewProxy

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

https://i-blog.csdnimg.cn/direct/2e16b416375a40c3a7492a668f47d26f.png
核心代码:refreshview视图和列表适配器、点击评价类型革新数据、革新视图、上划下拉革新视图
// 初始化总评分星星的正常和选中状态图片
int size = DpUtil.dp2px(10);
Bitmap starNormal = BitmapUtil.thumbImageWithMatrix(getResources(), R.drawable.icon_evaluate_default, size, size);
Bitmap starFocus = BitmapUtil.thumbImageWithMatrix(getResources(), R.drawable.icon_evaluate_select, size, size);
mStar.setNormalImg(starNormal);
mStar.setFocusImg(starFocus);

// 设置评价类型按钮组的监听器
mVpBtnEvaluate.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
      // 根据选中的按钮切换评价类型
      if(checkedId==R.id.btn_total){
            checkType(ShopState.COMMENTS_TOTAL);
      }else if(checkedId==R.id.btn_best){
            checkType(ShopState.COMMENTS_GOODS);
      }else if(checkedId==R.id.btn_normal){
            checkType(ShopState.COMMENTS_NORMAL);
      }else if(checkedId==R.id.btn_bad){
            checkType(ShopState.COMMENTS_BAD);
      }
    }
});

mEvaluateListAdapter=new EvaluateListAdapter(null,getResources());
mEvaluateListAdapter.setStringOnItemClickListener(new ViewGroupLayoutBaseAdapter.OnItemClickListener<String>() {
    @Override
    public void onItemClicked(ViewGroupLayoutBaseAdapter<String> adapter, View v, String item, int position) {
      // 点击评价中的图片时显示图片画廊
      List<String>urlList=adapter.getData();
      showGally(urlList,position);
    }
});

// 设置刷新视图的适配器和布局管理器
mRefreshView.setAdapter(mEvaluateListAdapter);
mRefreshView.setReclyViewSetting(RxRefreshView.ReclyViewSetting.createLinearSetting(this,1));
mRefreshView.setDataListner(new RxRefreshView.DataListner<EvaluateBean>() {
    @Override//适配器的data不是在新建适配器的时候放入,而是使用dataListener使用getData方法从服务器获取数据
    //当initData、下拉或者上划时,会调用loadData方法,并传入当前页码p,然后返回一个Observable对象,
    public Observable<List<EvaluateBean>> loadData(int p) {
      // 加载评价数据
      //这个方法负责从服务器获取评价数据,并返回一个 Observable<List<EvaluateBean>>。
      // 当数据加载完成后,RxRefreshView 会调用 compelete 方法来更新适配器。
      return getData(p);
    }
推荐标签 GoodsDetailRecommendViewProxy

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

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


[*]把每页的RecyclerView设置适配器GoodsAdapter
[*]设置好每页的布局(网格布局一页每行3个)、分割线
[*]设置goodsAdapter点击变乱
//初始化RecyclerView
private GoodsAdapter initReclyView(RecyclerView recyclerView) {
    GoodsAdapter goodsAdapter=new GoodsAdapter(null);
    recyclerView.setAdapter(goodsAdapter);
    //配置LayoutManager,每行3个,一页展示6个,所以是2行
    GridLayoutManager gridLayoutManager= new GridLayoutManager(getActivity(),3){//每行3个
      @Override
      public boolean canScrollVertically() {
            return false;
      }
    };
    //添加分割线
    ItemDecoration decoration = new ItemDecoration(getActivity(), 0xffdd00, 10, 10);
    //recyclerView是引用类型,所以这里修改recyclerView会直接影响到BannerAdapter的recyclerView
    recyclerView.setLayoutManager(gridLayoutManager);
    recyclerView.addItemDecoration(decoration);
    //设置点击事件监听器
    goodsAdapter.setOnItemClickListener(GoodsDetailRecommendViewProxy.this);
    return goodsAdapter;
} 第三层:GoodsAdapter是每个页面中每个商品项的适配器。把当前页的List的6个GoodsBean做适配。负责把拿到的List分成每个商品项GoodsBean适配
public class GoodsAdapter extends BaseRecyclerAdapter<GoodsBean,BaseReclyViewHolder>{
    public GoodsAdapter(List<GoodsBean> data) {
      super(data);
    }

    //获取布局ID
    @Override
    public int getLayoutId() {
      return R.layout.item_recly_goods_recommend;
    }
    //转换视图,设置数据
    @Override
    protected void convert(@NonNull BaseReclyViewHolder helper, GoodsBean item) {//第三层GoodsBean
      helper.setImageUrl(item.getThumb(),R.id.img_cover);
      helper.setText(R.id.tv_title,item.getName());
      helper.setText(R.id.tv_price,item.getUnitPrice());
    }
}
商品详情GoodsWebViewProxy

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


直播间界面LiveAudienceActivity

https://i-blog.csdnimg.cn/direct/70c6c10bb7f4435d8554a56ec6a7cbe0.png
private MyViewPager mViewPager;//两个页面,一个空界面一个ui界面,可左右滑动
private ViewGroup mSecondPage;//默认显示第二页(包含了底层和顶层布局)
private LiveAudienceViewHolder mLiveAudienceViewHolder;// 初始化观众直播间底部视图(聊天、礼物、分享、点赞飘心心)
protected LiveRoomViewHolder mLiveRoomViewHolder;// 初始化直播间顶层页面框架(评论、主播头像、点赞量、本场在售商品、在线观众、退出)

/**
* 主函数入口
* 初始化音频流、界面布局和视图持有者
*/
@Override
protected void main() {
    // 确保音量控制器在播放音乐时控制音量,而不是系统的其他音量
    setVolumeControlStream(AudioManager.STREAM_MUSIC);
    // 如果使用滚动布局
    if (isUseScroll()) {
      // 初始化RecyclerView并设置固定大小和垂直线性布局管理器
      mRecyclerView = super.findViewById(R.id.recyclerView);
      mRecyclerView.setHasFixedSize(true);
      mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));
      // 为直播观众界面布局
      mMainContentView = LayoutInflater.from(mContext).inflate(R.layout.activity_live_audience, null, false);
    }
    // 调用父类的main方法进行初始化
    super.main();
    // 初始化播放容器和主容器
    mPlayContainer = (ViewGroup) findViewById(R.id.play_container);//总直播间容器
    mContainer = (FrameLayout) findViewById(R.id.container);
    // 初始化直播播放视图持有者并添加到父容器
    mLivePlayViewHolder = new LivePlayTxViewHolder(mContext, mPlayContainer);
    mLivePlayViewHolder.addToParent();
    //让这个视图持有者订阅 Activity 的生命周期,确保在活动的生命周期内适时地清理和更新视图。
    mLivePlayViewHolder.subscribeActivityLifeCycle();
    // 初始化ViewPager和第二个页面布局
    mViewPager = (MyViewPager) findViewById(R.id.viewPager);
    mSecondPage = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.view_audience_page, mViewPager, false);
    mContainerWrap = mSecondPage.findViewById(R.id.container_wrap);
    //mContainer是mSecondPage的组件,包含LiveRoomViewHolder和LiveAudienceViewHolder
    mContainer = mSecondPage.findViewById(R.id.container);
    // 初始化直播间顶层页面框架(评论、主播头像、点赞量、本场在售商品、在线观众、退出)
    mLiveRoomViewHolder = new LiveRoomViewHolder(mContext, mContainer, (GifImageView) mSecondPage.findViewById(R.id.gift_gif), (SVGAImageView) mSecondPage.findViewById(R.id.gift_svga), mContainerWrap);
    mLiveRoomViewHolder.addToParent();
    mLiveRoomViewHolder.subscribeActivityLifeCycle();
    // 初始化观众直播间底部视图(聊天、礼物、分享、点赞)
    mLiveAudienceViewHolder = new LiveAudienceViewHolder(mContext, mContainer);
    mLiveAudienceViewHolder.addToParent();
    mLiveAudienceViewHolder.setUnReadCount(getImUnReadCount());
    mLiveBottomViewHolder = mLiveAudienceViewHolder;
    // 设置ViewPager适配器
    mViewPager.setAdapter(new PagerAdapter() {

      @Override
      public int getCount() {
            return 2;
      }

      @Override
      public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
            return view == object;
      }

      @NonNull
      @Override
      public Object instantiateItem(@NonNull ViewGroup container, int position) {
            if (position == 0) {
                //第一页是空View空页面,用于右滑展示单独的直播视频,
                View view = new View(mContext);
                view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                container.addView(view);
                return view;
            } else {
                //第二页是mSecondPage,意思是mSecondPage包含了所有ui按键层的布局
                container.addView(mSecondPage);
                return mSecondPage;
            }
      }

      @Override
      public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
      }
    });
    // 设置ViewPager当前项为第二个页面
    mViewPager.setCurrentItem(1);
    // 初始化直播连麦相关 presenter
    mLiveLinkMicPresenter = new LiveLinkMicPresenter(mContext, mLivePlayViewHolder, false, mLiveSDK, mLiveAudienceViewHolder.getContentView());
    mLiveLinkMicAnchorPresenter = new LiveLinkMicAnchorPresenter(mContext, mLivePlayViewHolder, false, mLiveSDK, null);
    mLiveLinkMicPkPresenter = new LiveLinkMicPkPresenter(mContext, mLivePlayViewHolder, false, null);
    // 如果使用滚动布局
    if (isUseScroll()) {
      // 获取直播数据列表并初始化滚动适配器
      List<LiveBean> list = LiveStorge.getInstance().get(mKey);
      mRoomScrollAdapter = new LiveRoomScrollAdapter(mContext, list, mPosition);
      mRoomScrollAdapter.setActionListener(new LiveRoomScrollAdapter.ActionListener() {
            @Override
            public void onPageSelected(LiveBean liveBean, ViewGroup container, boolean first) {
                L.e(TAG, "onPageSelected----->" + liveBean);
                // 当选中直播页面时,更新主内容视图的父容器
                if (mMainContentView != null && container != null) {
                  ViewParent parent = mMainContentView.getParent();
                  if (parent != null) {
                        ViewGroup viewGroup = (ViewGroup) parent;
                        if (viewGroup != container) {
                            viewGroup.removeView(mMainContentView);
                            container.addView(mMainContentView);
                        }
                  } else {
                        container.addView(mMainContentView);
                  }
                }

            }

            @Override
            public void onPageOutWindow(String liveUid) {
                L.e(TAG, "onPageOutWindow----->" + liveUid);
                // 当页面移出窗口时,取消相关HTTP请求并清理房间数据
                if (TextUtils.isEmpty(mLiveUid) || mLiveUid.equals(liveUid)) {
                  LiveHttpUtil.cancel(LiveHttpConsts.CHECK_LIVE);
                  LiveHttpUtil.cancel(LiveHttpConsts.ENTER_ROOM);
                  LiveHttpUtil.cancel(LiveHttpConsts.ROOM_CHARGE);
                  clearRoomData();
                }
            }
      });
      // 设置RecyclerView适配器
      mRecyclerView.setAdapter(mRoomScrollAdapter);
    }
    // 如果有直播数据,则设置直播间数据并进入房间
    if (mLiveBean != null) {
      setLiveRoomData(mLiveBean);
      enterRoom();
    }
}

/**
* 点亮飘心心,被LiveAudienceViewHolder和LiveRoomViewHolder调用
*/
public void light() {
    if (!mLighted) {
      mLighted = true;
      SocketChatUtil.sendLightMessage(mSocketClient, 1 + RandomUtil.nextInt(6));
    }
    if (mLiveRoomViewHolder != null) {
      mLiveRoomViewHolder.playLightAnim();
    }
    setLikes();

}
/**LiveAudienceViewHolder和LiveRoomViewHolder的点击事件   */
@Override
public void onClick(View v) {
    int i = v.getId();

    if (i == R.id.root) {//当点到页面上除了控件外的地方时(根布局)
      light();//弹出点亮心心动画
    }
    if (!canClick()) {
      return;
    }
    if (i == R.id.avatar) {
      if (mLiveActivity!=null){
            mLiveActivity.showAnchorUserDialog(mIsFollowAnthor);
      }
    } else if (i == R.id.btn_follow) {
      follow();
    } else if (i == R.id.btn_goods) {
      openShopGoods();
    }else if(i==R.id.btn_close){//点击右上角叉,退出直播间
      close();
    }else if(i==R.id.tv_user_count){
      openUserList();
    }
} 四个核心布局/容器:


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


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

悬浮窗功能checkPermissonOpenNarrow

https://i-blog.csdnimg.cn/direct/41d1103236c1487087d0924bc515bfdb.pnghttps://i-blog.csdnimg.cn/direct/b7a98c466e104b498549e04000e808fc.png

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 弹出对话框,提示用户去设置中开启权限。


https://i-blog.csdnimg.cn/direct/7df25fb4ada94f369e24577deb842021.png
https://i-blog.csdnimg.cn/direct/fd086a9e8b2b43988138717d425db5a5.png
public void checkPermissonOpenNarrow(Context context, boolean needRequestPermisson, final boolean needLockTouch) {
//needRequestPermisson是选择是否弹出请求对话框(举报界面false就不会弹出对话框,返回键和图标叉true就会弹出)。
//needLockTouch如果是true:能拖不能点;如果是false:能拖能点
    if (mWindowAddHelper == null) {
      mWindowAddHelper = new WindowAddHelper(this);
    }
    mWindowAddHelper.checkOverLay(this, new Predicate<Boolean>() {
      @Override
      public boolean test(Boolean aBoolean) throws Exception {
            if (!aBoolean) {//注意这里的aBoolean是从checkOverLay里面的openMakeWindowsPermissonDialog(context)来的
                onBackAndFinish();
            }
            return aBoolean;
      }
    }, needRequestPermisson).subscribe(new Consumer<Boolean>() {
      @Override
      public void accept(Boolean aBoolean) throws Exception {
            if (aBoolean) {
                openNarrow(needLockTouch);
            } else if (!needLockTouch) {
                onBackAndFinish();
            }
      }
    });
} 2.1 checkOverLay 方法



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

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

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

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



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

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

private Observable<Boolean> openMakeWindowsPermissonDialog(final Context context){
    return Observable.create(new ObservableOnSubscribe<Boolean>() {
      @Override
      public void subscribe(ObservableEmitter<Boolean> e) throws Exception {
            openMakeWindowsPermissonDialog(context,e);
      }
    });
}

private void openMakeWindowsPermissonDialog(Context context,final ObservableEmitter<Boolean> e){
//弹出一个对话框,提示用户去设置中开启悬浮窗权限。
//这个对话框其实就是返回一个布尔值,这个布尔值用来判断下面是否要打开悬浮窗设置
    Dialog dialog= new DialogUitl.Builder(context)
            .setTitle("")
            .setContent("你的手机没有授权浮窗权限,是否前往申请?")
            .setCancelable(true)
            .setBackgroundDimEnabled(true)
            .setCancelString("关闭直播间")
            .setConfrimString("立即开启")
            .setClickCallback(new DialogUitl.SimpleCallback2() {
                @Override
                public void onConfirmClick(Dialog dialog, String content) {
                  e.onNext(true);
                }
                @Override
                public void onCancelClick() {
                  e.onNext(false);
                }
            })
            .build();
    dialog.show();
}
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)推到前台
private void openNarrow(final boolean needLockTouch) {//悬浮窗显示的核心逻辑
    //initReceiver();
    isFloatAtWindow = true;
    initWindowMannger();
    final View view = exportFlowView();
    final WindowManager.LayoutParams layoutParams = mWindowAddHelper.createDefaultWindowsParams(0, 100);
    if (view != null) {
      moveTaskToBack(false);//当前界面退到后台
      int time = delayToFloatWindowTime();//默认1秒
      if (time <= 0) {
            createNarrowWindow(layoutParams, view, needLockTouch);
      } else {
            mDisposable = Observable.timer(time, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Long>() {
                @Override
                public void accept(Long aLong) throws Exception {
                  createNarrowWindow(layoutParams, view, needLockTouch);
                }
            });
      }
    }
}

private void createNarrowWindow(WindowManager.LayoutParams layoutParams, View view, boolean needLockTouch) {
    mWindowsFloatLayout = new FloatFrameLayout(mContext);//创建一个 FloatFrameLayout,用于承载悬浮窗的内容
    mWindowsFloatLayout.setLock(needLockTouch);//是否能点击
    makeParm(mWindowsFloatLayout, layoutParams, view);//调整悬浮窗的布局参数(如宽度、高度等)
    mWindowsFloatLayout.setView(view, 0);//将导出的 View 添加到 FloatFrameLayout 中
    mWindowsFloatLayout.setWmParams(layoutParams);//将布局参数应用到悬浮窗
    mWindowManager.addView(mWindowsFloatLayout, layoutParams);//将 FloatFrameLayout 添加到屏幕上,显示悬浮窗
    mWindowsFloatLayout.setOnNoTouchClickListner(new FloatFrameLayout.OnNoTouchClickListner() {//
      @Override
      public void click(View view) {//点击悬浮窗(而不是拖动),会触发 FloatFrameLayout 的 OnNoTouchClickListner
            restoreVideoFromWindowFlat(view);
            //将悬浮窗中的 View 重新添加到 LiveAudienceActivity 的布局中,恢复全屏直播窗口
      }

      @Override
      public void close(View view) {
            onBackAndFinish();
      }//悬浮窗右上角有一个关闭按钮,关闭悬浮窗并退出直播
    });
}
/** 设置参数layoutParams(可能是调整?) */
private void makeParm(FloatFrameLayout windowsFloatLayout, WindowManager.LayoutParams layoutParams, View view) {
    Utils.initFloatParamList(this);
    layoutParams.width = Utils.subWidth != 0 ? Utils.subWidth : view.getWidth();
    layoutParams.height = Utils.subHeight != 0 ? Utils.subHeight + DpUtil.dp2px(20) : view.getHeight();
}

/*恢复界面控件到activity界面*/
private void restoreVideoFromWindowFlat(View view) {
    if (mWindowManager == null || !isFloatAtWindow) {//只有windowManger 不为空,并且isFloatAtWindow为true才不会被return
      return;
    }
    try {
      isFloatAtWindow = false;//清理悬浮窗的状态
      mWindowManager.removeView(view);//移除悬浮窗
      /*从前台点击*/
      if (!ActivityMannger.getInstance().isBackGround()) {//检查当前应用是否在前台
            //将主栈顶的活动(LiveAudienceActivity)推到前台
            ActivityMannger.getInstance().launchOntherStackToTopActivity(false, ActivityMannger.getInstance().getMainStackTopActivity());
      }
      if (!isDestroyed()) {//如果活动未被销毁
            restoreFlowView((FloatFrameLayout) view);//将悬浮窗中的视图重新添加到 mPlayContainer 中,恢复全屏直播窗口
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
} 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

https://i-blog.csdnimg.cn/direct/6e85985be59d47078a2a8939ed7bac57.pnghttps://i-blog.csdnimg.cn/direct/2e18456a372c4746832b95c1a4cd7bd0.png
ShopCartActivity部分



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


[*]获取数据到适配器、空数据处置惩罚
mShopCartAdapter=new ShopCartAdapter(null,getViewProxyMannger(),this,mShopCartModel);
mRefreshView.setAdapter(mShopCartAdapter);
mRefreshView.setRefreshEnable(false);
mRefreshView.setReclyViewSetting(RxRefreshView.ReclyViewSetting.createLinearSetting(this,0));
mRefreshView.setDataListner(new RxRefreshView.DataListner<MultiItemEntity>() {
    @Override
    public Observable<List<MultiItemEntity>> loadData(int p) {//mRefreshView获取数据
      //在RxRefreshView中的抽象方法loadData() 方法的具体实现是在 ShopCartActivity 中提供的,它调用了 getData 方法来获取数据
      //RefreshView中的refresh()或者loadMore()会使用到loadData获取到的数据Observable<List<MultiItemEntity>>
      //在订阅的时候会调用shopCartAdapter的setData方法,把数据绑定到视图中
      return getData();
    }
    @Override
    public void compelete(List<MultiItemEntity> data) {//获取数据完成时,把数据绑定到视图
      //当 mRefreshView 完成数据加载并调用 compelete(List<MultiItemEntity> data) 方法时
      // List<MultiItemEntity> 会被传递给 ShopCartAdapter
      mShopCartAdapter.expandAll();
      if(ListUtil.haveData(data)){
         ViewUtil.setVisibility(mVpBottom,View.VISIBLE);//如果有商品数据,就显示底部栏(立即下单)
      }else{
         ViewUtil.setVisibility(mVpBottom,View.GONE);//如果没有商品数据,就隐藏底部栏
      }
         notifyAllSelectButton();
    }
   
    /*网络请求,api获取数据*/
private Observable<List<MultiItemEntity>> getData() {
    return ShopAPI.getShopCartData().map(new Function<ShopcartParseBean, List<MultiItemEntity>>() {
      @Override
      public List<MultiItemEntity> apply(ShopcartParseBean shopcartParseBean) throws Exception {
            mShopcartParseBean=shopcartParseBean;
            List<MultiItemEntity>list=ShopCartModel.transFormListData(shopcartParseBean);
            if(mShopCartModel!=null){
               mShopCartModel.setShopCartData(shopcartParseBean.getValid());// Valid有效商品列表给VM
               //将有效的商品数据设置到 ShopCartModel 中,以便后续操作(如计算总价等)
            }
            return list;
      }
    }).compose(this.<List<MultiItemEntity>>bindUntilOnDestoryEvent());
}

[*]页面按键点击变乱,处置惩罚页面整体变乱(一个按键一个变乱,当用不到的时间就隐藏)
/**
* 其实每一个按键都是有单独的点击事件,没有变换按键的逻辑。只不过点击管理的时候变换或隐藏了布局
* 让有些按键不能点击了。
* 设置按键布局的显示和隐藏是一种优化,避免了一个按键的不同情况不同事件的逻辑。
* @param v
*/
@Override
public void onClick(View v) {
    if(!ClickUtil.canClick()){
      return;
    }
    int id=v.getId();
    if(id==R.id.btn_mannger){
      judgeState();//切换购物车的编辑状态
    }else if(id==R.id.btn_collect){
      collect();//收藏
    }else if(id==R.id.btn_delete){
       deleteGoods();//删除选中的商品
    }else if(id==R.id.check_total_image){//全选框
      judgeAllSelect();//判断是否全选所有商品,并更新复选框状态
    }else if(id==R.id.btn_commit){
      commit();//提交选中的商品,进入订单确认页面
    }
}

/**
* 提交选中的商品,进入订单确认页面
*/
private void commit() {
   if(mShopCartModel==null){
         return;
   }

   String[] selectId=mShopCartModel.getAllSelectCartId();//获取选中的id
   if(selectId==null||selectId.length<=0){
         ToastUtil.show(getString(R.string.select_goods_tip));
         return;
   }
   String idArray=StringUtil.splitJoint(selectId);
   CommitOrderActivity.forward(this,idArray);

}

private void judgeAllSelect() {
   if(mCheckTotalImage==null){
         return;
   }
   final boolean isTargetCheck=!mCheckTotalImage.isChecked();//判断下一次点击是选中还是取消(如果本来选中了,下一次点击就是取消)
   mShopCartModel.setAllSelected(isTargetCheck);//如果原本没有选中,就让商品全部选中。如果原本有选中,商品就全部取消选中
   mCheckTotalImage.setChecked(isTargetCheck);//设置选中状态
}

/*删除商品*/
private void deleteGoods() {
   final String[]allSelectId=mShopCartModel.getAllSelectCartId();//还是获取选中id
   if(allSelectId==null||allSelectId.length<=0) {
         ToastUtil.show(R.string.select_goods_tip);
         return;
   }
   DialogUitl.showSimpleDialog(this, "是否要删除商品?", new DialogUitl.SimpleCallback() {
         @Override
         public void onConfirmClick(Dialog dialog, String content) {
             if(mShopCartModel!=null){
               mShopCartModel.deleteGoodsArray(allSelectId,ShopCartActivity.this);
             }
         }
   });
}

/*批量收藏商品*/
private void collect() {
   String[] allSelectId=getAllSelectGoodsId();//获取选中的商品id
    if(allSelectId==null||allSelectId.length<=0) {
      ToastUtil.show(getString(R.string.select_goods_tip));//如果没有商品被选中就提示
      return;
    }
    ShopAPI.batchCollect(allSelectId, ShopState.PRODUCT_DEFAULT).//api接入收藏商品
            compose(this.<Boolean>bindUntilOnDestoryEvent())
            .subscribe(new DefaultObserver<Boolean>() {
                @Override
                public void onNext(Boolean aBoolean) {
                  if(aBoolean){
                      ToastUtil.show(R.string.collect_succ);
                  }
                }
            });
}

private String[] getAllSelectGoodsId() {//封装获取所有选中的商品id
   if(mShopCartModel==null){
         return null;
   }
   String[] allSelectId=mShopCartModel.getAllSelectGoodsId();
   return allSelectId;
}
适配器ShopCartAdapter

适配器是设置RxRefreshView列表的列表项的
public class ShopCartAdapter <T extends MultiItemEntity> extends BaseMutiRecyclerAdapter<T, BaseReclyViewHolder>{
    //ShopCartAdapter--BaseMutiRecyclerAdapter--BaseMultiItemQuickAdapter--BaseQuickAdapter
    //只要适配器继承了 BaseQuickAdapter,然后在 convert() 里用 addOnClickListener(viewId) 绑定子视图
    // 就可以用 setOnItemChildClickListener() 监听它,而不会触发 setOnItemClickListener()
    // 因为 BaseQuickAdapter 已经封装好了事件分发逻辑
    private BaseProxyMannger mBaseProxyMannger;
    private Context mContext;
    private ShopCartModel mShopCartModel;

    /**
   * @param data
   * @param baseProxyMannger
   * 对于复杂的、封装过的功能组件(如 ShopCartGoodsNumViewProxy),可以使用 BaseProxyMannger 来管理和复用这些组件。
   * 这种方式适用于需要频繁创建和销毁的视图组件,或者具有复杂交互逻辑的组件。通过 BaseProxyMannger,可以更好地管理和复用这些组件,简化事件监听器的绑定,并提高性能。
   * @param context
   * @param shopCartModel
   */
    public ShopCartAdapter(List<T> data,BaseProxyMannger baseProxyMannger,Context context,ShopCartModel shopCartModel) {
      super(data);
      mContext=context;
      mBaseProxyMannger=baseProxyMannger;//一个视图代理管理器,用于动态管理和复用视图组件(如商品数量选择器)
      //其实只要记住:BaseProxyMannger在商品item中用来管理商品数量选择器就行了,不太需要过多了解
      mShopCartModel=shopCartModel;

/*过期状态的layout*/
addItemType(ShopCartStoreBean.TYPE_INVALID, R.layout.item_recly_shop_cart_invalid_title);
addItemType(ShopCartBean.TYPE_INVALID,R.layout.item_relcy_shop_cart_invaild_goods);

/*正常状态的layout*/
addItemType(ShopCartBean.TYPE_VALID,R.layout.item_relcy_shop_cart_goods);//商品item
addItemType(ShopCartStoreBean.TYPE_VALID,R.layout.item_recly_shop_cart_store);//店铺item,点到就全选

[*]设置商品和商品选项框的点击变乱
setOnItemClickListener(new OnItemClickListener() {//监听每个商品item点击事件(选项框除外)
    @Override
    public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
      MultiItemEntity multiItemEntity=getItem(position);
      if(multiItemEntity==null){
            return;
      }
      int itemType=multiItemEntity.getItemType();
      if(multiItemEntity instanceof ShopCartStoreBean){
            if(itemType==ShopCartStoreBean.TYPE_INVALID){//如果点到失效商品
               clickInVaildHead(multiItemEntity,position);//点击失效商品进行开关
            }else{
               clickStore(multiItemEntity,view,position);//点击店铺
            }
      }else if(multiItemEntity instanceof ShopCartBean){
            if(itemType==ShopCartBean.TYPE_VALID){
               clickGoods(multiItemEntity,view,position);//点击商品跳转到商品详情页
            }
      }
    }
});

setOnItemChildClickListener(new OnItemChildClickListener() {//监听选项框的点击事件
    @Override
    public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
      MultiItemEntity multiItemEntity=getItem(position);
      if(multiItemEntity==null){
            return;
      }
      int type=multiItemEntity.getItemType();
      if(type==ShopCartStoreBean.TYPE_VALID){
            clickStore(multiItemEntity,view,position);//全选店铺对应商品
      }else if(type==ShopCartBean.TYPE_VALID){
            clickCheckGoods(multiItemEntity,view,position);//点击切换商品选中状态
      }else if(type==ShopCartStoreBean.TYPE_INVALID){
            deleteAllInVidGood();
      }
    }
}); setOnItemClickListener和setOnItemChildClickListener是用到了BaseQuickAdapter,可以给列表项(这里是商品项)和列表项的子项(商品的选项框)分别实现不同的点击变乱
只需要在convert方法中标记ItemChild,就可以使用setOnItemChildClickListener为子项设置setOnItemChildClickListener
helper.addOnClickListener(R.id.check_image);// 标记 CheckImageView 为 ItemChild

[*]适配器的convert绑定数据item和视图BaseReclyViewHolder
/*过期状态的layout*/
addItemType(ShopCartStoreBean.TYPE_INVALID, R.layout.item_recly_shop_cart_invalid_title);
addItemType(ShopCartBean.TYPE_INVALID,R.layout.item_relcy_shop_cart_invaild_goods);

/*正常状态的layout*/
addItemType(ShopCartBean.TYPE_VALID,R.layout.item_relcy_shop_cart_goods);//商品item
addItemType(ShopCartStoreBean.TYPE_VALID,R.layout.item_recly_shop_cart_store);//店铺item,点到就全选
/**
* 对于简单的、基础的视图组件(如 TextView、ImageView、CheckImageView 等),
* 可以直接在 convert 方法中通过 BaseReclyViewHolder 提供的方法进行绑定和更新。
* 这种方式简洁明了,适合处理较为简单的视图状态变化。
* 简单直接:代码逻辑清晰,易于理解和维护。 性能高效:避免了不必要的复杂操作,提高了性能。
* @param helper A fully initialized helper.
* @param item   The item that needs to be displayed.
*/
@Override
protected void convert(@NonNull BaseReclyViewHolder helper, T item) {//在convert中要用helper.setXX绑定视图
    //每个 convertXXX 方法负责将具体的 ShopCartStoreBean 或 ShopCartBean
    //在 ShopCartAdapter 中,convert 方法根据不同的 itemType 调用不同的转换方法,以处理不同类型的数据项,确保代码结构清晰且易于维护。
    //上面使用addItemType设置不同数据项的布局,在这里可以根据不同类型数据项的不同布局来绑定对应的数据。区分地绑定数据和布局
    switch (item.getItemType()){
      case ShopCartStoreBean.TYPE_INVALID:
            convertStoreInvalid(helper,item);
            break;
      case ShopCartStoreBean.TYPE_VALID:
            convertStoreValid(helper,item);
            break;
      case ShopCartBean.TYPE_INVALID:
            convertGoodsInvalid(helper,item);
            break;
      case ShopCartBean.TYPE_VALID:
            convertGoodsValid(helper,item);
            break;
      default:
            break;
    }
}
/*正常商品的店铺*/
private void convertStoreValid(BaseReclyViewHolder helper, T item) {
    ShopCartStoreBean shopCartStoreBean= (ShopCartStoreBean) item;
    helper.setText(R.id.tv_name,shopCartStoreBean.getName());//绑定店铺名称
    CheckImageView checkImageView=helper.getView(R.id.check_image);
    checkImageView.setChecked(shopCartStoreBean.isChecked());//绑定选定状态
}

/*正常商品展示*/
private void convertGoodsValid(BaseReclyViewHolder helper, T item) {
    ShopCartBean shopCartBean= (ShopCartBean) item;
    GoodsBean goodsBean=shopCartBean.getProductInfo();
    View container=helper.getView(R.id.container);
    if(goodsBean!=null){
      helper.setText(R.id.tv_title,goodsBean.getName());//商品标题
      helper.setText(R.id.tv_price,goodsBean.getUnitPrice());//商品价格
      helper.setImageUrl(goodsBean.getThumb(),R.id.img_thumb);//商品图片
      SpecsValueBean specsValueBean=goodsBean.getAttrInfo();
      if(specsValueBean!=null){
          helper.setText(R.id.tv_field, WordUtil.getString(R.string.goods_field_tip,specsValueBean.getSuk()));
      }
    }

    CheckImageView checkImageView=helper.getView(R.id.check_image);
    checkImageView.setChecked(shopCartBean.isChecked());
    ViewGroup nameContainer=helper.getView(R.id.vp_number_container);// 商品数量容器(+/-)
    helper.addOnClickListener(R.id.check_image);// 标记 CheckImageView 为 ItemChild
    if(mBaseProxyMannger==null||container==null){
      return;
    }

    ShopCartGoodsNumViewProxy goodsNumViewProxy=null;
    String tag=Integer.toString(container.hashCode());
    BaseViewProxy baseViewProxy=mBaseProxyMannger.getViewProxyByTag(tag);//检查是否存在与指定 tag 对应的视图代理
    if(baseViewProxy!=null){
      goodsNumViewProxy= (ShopCartGoodsNumViewProxy)baseViewProxy ;
    }else{
      goodsNumViewProxy=new ShopCartGoodsNumViewProxy();
      goodsNumViewProxy.setEableDelay(true);
      goodsNumViewProxy.setNumberChangeListnter(mNumberChangeListnter);
      mBaseProxyMannger.addViewProxy(nameContainer,goodsNumViewProxy,tag);//把商品数量容器布局跟商品item里的视图vp_number_container绑定
      //这里只需要知道商品数量选择器的功能视图goodsNumViewProxy跟商品item的布局vp_number_container绑定就行
    }
       goodsNumViewProxy.setShopCartBean(shopCartBean);
}

/*过期商品展示*/
private void convertGoodsInvalid(BaseReclyViewHolder helper, T item) {
    ShopCartBean shopCartBean= (ShopCartBean) item;
    GoodsBean goodsBean=shopCartBean.getProductInfo();
    if(goodsBean!=null){
      helper.setText(R.id.tv_title,goodsBean.getName());
      //helper.setText(R.id.tv_price,goodsBean.getUnitPrice());
      helper.setImageUrl(goodsBean.getThumb(),R.id.img_thumb);
    }

}
/**监听商品数字变化*/
GoodsNumViewProxy.NumberChangeListnter<ShopCartGoodsNumViewProxy> mNumberChangeListnter= new GoodsNumViewProxy.NumberChangeListnter<ShopCartGoodsNumViewProxy> () {
    @Override
    public void change(ShopCartGoodsNumViewProxy goodsNumViewProxy, int num) {
       if(mShopCartModel!=null&&goodsNumViewProxy.haveData()){
          ShopCartBean shopCartBean=goodsNumViewProxy.getShopCartBean();
          mShopCartModel.modifyCartNum(shopCartBean,num);//更新数据
       }
    }
};
/*过期商品统统归类为过期商品店铺*/
private void convertStoreInvalid(BaseReclyViewHolder helper, T item) {
    final ShopCartStoreBean level0Bean= (ShopCartStoreBean) item;
    View arrowView=helper.getView(R.id.img_arrow);
    if(level0Bean.isExpanded()){
      arrowView.setRotation(180);
    }else{
      arrowView.setRotation(0);
    }
    helper.addOnClickListener(R.id.btn_delete);
}








免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Android企业级开源电商+直播app视图层全解析(保姆级完整版)