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&§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的层级关系:标签栏和子商品是上下级
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]