涵盖技术点:Banner+MagicIndicator+viewpager+RecyclerView+RxRefreshView(视图革新)+LayoutManager(标签列表布局定位)+自定义视图+Bitmap+WebView(H5)+WebView(视频流)+RxJava变乱流/观察者模式+悬浮窗(WindowManager+FloatFrameLayout)
首页界面
- 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 为圆角大小。
轮播图指示器
- 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());
复制代码 就把轮播图放到首页上了
标签栏的每个标签对应不同类型的直播间列表
- 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() 把这些内容添补到对应的活页页面上。
商品分类界面
初始布局设置
- 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&§ionBean.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的层级关系:标签栏和子商品是上下级
右侧List<ClassifySectionBean> sectionBeanList 层级关系:标签头和商品是平级关系
左侧标签栏和右侧列表的联动
左侧标签栏点击,右侧滑动到相应标签头
- 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
初始化轮播图
- mBanner.setAutoPlay(false).setIndicator(defaultIndicator())//适配指示器
- .setAdapter(mImageAdapter);//适配器
-
- mImageAdapter.setData(bannerList);//把图片数据装入适配器
复制代码 商品标签大部分简朴数据直接从mStoreGoodsBean获取
- 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商品规格选择
- //规格选择难点:规格尺寸适配器
- SpecSelectAdapter adapter = new SpecSelectAdapter(specsBeanList,key, getViewProxyMannger().getLayoutInflater());
复制代码
初始化:获取默认选项,适配每一类的规格选择框(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[layoutPosition] = 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评价具体界面

核心代码: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
大概流程:拿到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

- 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
1. 触发悬浮窗
悬浮窗的触发通常有两种方式:
:通过 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 弹出对话框,提示用户去设置中开启权限。
- 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){
- return openMakeWindowsPermissonDialog(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();
- }
- });
- }
- return Observable.just(isDrawOverLays);
- }else{
- return Observable.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. 体现悬浮窗
方法是悬浮窗体现的核心逻辑:
- 调用 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 拖动悬浮窗
的 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. 悬浮窗的权限管理
负责悬浮窗的权限管理:
:检查当前是否具有悬浮窗权限。
:跳转到系统设置页面,哀求悬浮窗权限。
- openMakeWindowsPermissonDialog
:弹出对话框,提示用户去设置中开启权限。
8. 悬浮窗的流程图
以下是悬浮窗功能的简化流程图:
- 调用 checkPermissonOpenNarrow 检查悬浮窗权限。
- 如果权限未授予,弹出对话框提示用户去设置中开启权限。
- 调用 openNarrow,初始化悬浮窗参数并体现悬浮窗。
- 用户拖动悬浮窗或点击悬浮窗。
- 点击悬浮窗恢复全屏直播窗口,点击关闭按钮退出直播。
- 调用restoreVideoFromWindowFlat 或 clearFloatWindowState 清理悬浮窗状态。
购物车ShopCartActivity
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企服之家,中国第一个企服评测及商务社交产业平台。 |