android商品详情页开发

简介: 像商品详情这样的页面,功能多,页面繁杂,特别是对页面逻辑也不少,所以我觉得有必要记录一下开发商品详情页面踩过的坑。一.别人家的view如果是仿淘宝或京东的详情页那还好说image.png它的导航栏是在上边,这样的结构很好,基本不会有什么大问题,可以自定义一个布局去当标题栏。

像商品详情这样的页面,功能多,页面繁杂,特别是对页面逻辑也不少,所以我觉得有必要记录一下开发商品详情页面踩过的坑。

一.别人家的view

如果是仿淘宝或京东的详情页那还好说

img_2789ebccb1397c93132a1a86ed3bed3f.png
image.png

它的导航栏是在上边,这样的结构很好,基本不会有什么大问题,可以自定义一个布局去当标题栏。

关键是有些页面不是导航栏在上边,而是在中间(比如我自己要做的),这种情况其实不是很好,即使是能实现效果,但是体验还是不如JD那样的导航栏放上边的好。

img_e3a9879b54f97799f8921b8ee8f44009.png
image.png

比如这个taptap的详情页,导航栏就是放中间。

我这里只想说说这种导航栏在中间的情况

二.开发需求

如果是上边的导航栏在中间的情况,肯定会要求我们当滑动时,导航栏会一直顶在布局顶部。

1.用CoordinatorLayout实现布局

我们一看这样的布局,二话不说就马上能想到用CoordinatorLayout去实现这样的效果。没错,这样的布局讲道理应该是用CoordinatorLayout去实现,谷歌也是这样推荐的。

但是,我之前写过一篇文章说CoordinatorLayout有问题,当你折叠的部分高度不高时还不容易看出有什么问题,但是当可折叠部分高度高时,就会出现严重的滑动卡顿的问题,记住,是严重的卡顿。

可能有些大佬能够自定义Behavior来解决卡顿的问题。我也觉得这样的做法是官方的做法,但是我是新手嘛,自定义Behavior我反正试了没用,那只能走其它的路。

2.用Nestedscrollview实现布局

那我就用CoordinatorLayout的内部实现Nestedscrollview来解决这个问题,而Nestedscrollview官方定义本来就能解决滑动的冲突。

(1)自定义NestedScrollingParent和NestedScrollingChild

用Nestedscrollview的原理,我先自己写个NestedScrollingParent和NestedScrollingChild两个viewgroup来显示嵌套滑动的效果。

做法其实不难,就是要分别实现这两个接口的方法。

img_4a14941223112803777aaf0075aee292.png
image.png
img_46ab9832f8890ebd1b8c46f2a61c258c.png
image.png

然后你很容易在网上找到这两个接口中方法的使用流程。然后在自定义的viewgroup中完成事件监听onTouchEvent监听点击滑动放开。

我觉得没必要贴代码,就自定义NestedScrollingParent和NestedScrollingChild,网上有很多demo。主要做这些事:

实现接口中的方法
监听事件onTouchEvent

这样就能简单的实现上面说的效果(嵌套滑动并且导航栏会顶在布局顶部)。但是仅仅这样做会发现个问题,没有惯性。如果你仅仅只需要滑动流畅,那不做惯性也是一个不错的选择,但是没有惯性的滑动体验效果真的不是很好,也许是我们习惯了有惯性的滑动效果。

我看了下代码,惯性的实现和这两个接口关系不大,是要自己去实现。要做惯性就要用VelocityTracker这个类

img_6e280dcade25332e3aae1a8e936c7b53.png
image.png

意思就是这货能追踪触摸事件的速度,我之前没用过这个类,百度了一下资料,效果不是很理想,我尝试实现这个效果但是实际是没能实现的,毕竟没时间研究,以后肯定会写一篇关于这个的,毕竟它这么牛逼的效果。本来想去看看RecyclerView源码试试能不能看懂些什么,但是内聚性比较高加上一大堆静态变量,我还真看不出个所以然。

那么对于我来说用自定义NestedScrollingParent和NestedScrollingChild也失败了,因为我不会做惯性。那我就打算直接自定义NestedScrollingView,因为它内部已经有了惯性的机制。

(2)自定义NestedScrollingView充当NestedScrollingParent

首先我想说这个方法绝对可行,但是我做不到。我没办法让导航栏在滑动的时候停在顶部。

原因很简单,我做不到一件事:当父布局滑动到一定的位置时,子布局通知父布局不要滑动,而子布局来继续滑动,如果是自定义NestedScrollingView,我做不到子布局通知父布局不要滑动而自己滑动。也许是我对这个控件的了解不足,反正我试了很多个方法都不行,但是我觉得这个方法可行。

3.视觉效果实现布局

用CoordinatorLayout有官方的卡顿效果,用Nestedscrollview自己又不熟悉所以做不好,那怎么办,总不能不做吧。所以我就想出了第三种方法,这种方法能够实现那样的效果,只不过是投机取巧去实现。

(1)原理
总的来说还是使用Nestedscrollview嵌套,因为Nestedscrollview可以解决嵌套滑动的问题。那么怎么让图中的导航栏一直停在顶部呢?很简单,我只要做一个一模一样的布局一直放在顶部隐藏着,我监听滑动,当滑动的距离大于等于导航栏距顶部的距离,我就让隐藏的导航栏显示,这样就能产生视觉上的当导航栏滑到顶部时会一直在顶部的效果。

img_d4b0b5db5f0f52b77cf9ffd0295884a5.gif
15099406446461509940638249.gif

这个效果就是这样做出来的视觉差。

(2)实现

我们先来实现导航栏tabView吧。导航栏可以使用系统自带的tablayout,但是要注意,这个页面是用两个tablayout的,而且他们是联动的,就是说有一个tablayout切换到tab2的话,其它的tablayout都要切换到tab2。所以我们可以写一个帮助类来做TabLayout之间联动的操作。

我就暂时简单写一个,封装得不是很好。

public class ProductDetailsTabGroup {

    private Context context;
    private List<TabLayout> tabLayoutList;

    public ProductDetailsTabGroup(Context context){
        this.context = context;
        tabLayoutList = new ArrayList<>();
    }

    public void addTabLayout(TabLayout tabLayout){
        tabLayoutList.add(tabLayout);
    }

    public void addTitiles(String[] titles){

        if (tabLayoutList == null || tabLayoutList.size() < 1){
            return;
        }

        for (int i = 0; i < tabLayoutList.size(); i++) {
            for (int j = 0; j < titles.length; j++) {
                tabLayoutList.get(i).addTab(tabLayoutList.get(i).newTab().setText(titles[j]));
            }
        }

    }

    public void tabGroupListener(){

        if (tabLayoutList == null || tabLayoutList.size() < 1){
            return;
        }

        tabLayoutList.get(0).setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                tabLayoutList.get(1).getTabAt(tab.getPosition()).select();
                ((TestProductDetails)context).showFragment(tab.getPosition());
            }

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

            }

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

            }
        });

        tabLayoutList.get(1).setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                tabLayoutList.get(0).getTabAt(tab.getPosition()).select();
            }

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

            }

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

            }
        });

    }

}

addTitiles方法是所有tablayout设置相同的标题。tabGroupListener()方法是联动,我这里写死两个tab的联动,只用在其中一个加切换fragment的方法就行((TestProductDetails)context).showFragment(tab.getPosition())。

多个的时候用嵌套for循环来联动,我这里写死两个确实扩展性不好。

联动成功之后,监听滑动来判断顶部的tablayout的显示和隐藏。

scrollview.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
            @Override
            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                if (scrollY >= tabLayout.getTop()+contentView.getTop()+emptyViewGroup.getContentView().getTop()){
                    topTabLayout.setVisibility(View.VISIBLE);
                }else {
                    topTabLayout.setVisibility(View.GONE);
                }
            }
        });

(3)嵌套布局的Viewgroup

我想说说嵌套布局的viewgroup,用FragmentManager来做而不用viewpager来做,是因为会出现以下的原因:

如果使用viewpager的话,会出现布局高度不固定的情况。你可以设死一个固定的高度,但是这样的话,两个滚动会不兼容,就是会出现子布局的滚动会优先于父布局的滚动,而不是配合滚动。

但是这里有个技巧,你可以设置Viewpager的高度为根据子view的高度进行设置,这样的话就需要自定义viewpager重写onMeasure方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int height = 0;
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
                int h = child.getMeasuredHeight();
                if (h > height)
                    height = h;
            }

            mHight = height;
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

虽然这样能够解决高度的问题,但是这样做的话,或出现一个显现,假如有两个fragment,那viewpager的高度会取最后测量的那个,也就是说所有的fragment的高度会相同,如果偏低的页面就会补空白,偏高就会滚动。
这样就不行,我们需要的是每个fragment的高度都是自适应的。当然你也可以动态去改变viewpager的高度。

动态改变布局高度的方法是用setLayoutParams()

但是你要获取到布局的高度,需要用多线程来监听绘制后获取viewgroup的高度。

 ViewTreeObserver vto = viewgroup.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                rlParent.getViewTreeObserver().removeGlobalOnLayoutListener(this);
               // todo 获取viewgroup高度
            }
        });

虽然能实现,但是总的来说非常的麻烦,可能你不明白我说的是什么,但是如果你用viewpager来嵌套的话,就会出现很多问题,所以我建议用FragmentManager来做嵌套,而且你这样的页面中讲真也不应该给它左右滑动,不然会很乱。

三.总结

总的来说,实现第二张图那样的导航栏在中间的情况,真的会有很多坑,而且体验的效果还不如第一张图京东那样好。我也贴些代码吧,由于功能多,我只贴页面逻辑的代码。

1.布局

(1)最外层布局

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <com.xxxxx.xxxxx.components.widget.view.MyPullRefreshScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/scrollview"
        android:layout_above="@+id/ll_bottom"
        >
    </com.xxxxx.xxxxx.components.widget.view.MyPullRefreshScrollView>

    <android.support.design.widget.TabLayout
        android:layout_alignParentTop="true"
        android:id="@+id/tl_top_tab"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/white"
        android:visibility="gone"
        app:tabMode="fixed"
        app:tabGravity="fill"
        app:tabTextColor="@color/app_black"
        app:tabSelectedTextColor="@color/login_red"
        app:tabIndicatorColor="@color/login_red"
        app:tabIndicatorHeight="2dp"
      app:tabTextAppearance="@style/MyTabLayoutTextAppearanceInverse"
        />

</RelativeLayout>

MyPullRefreshScrollView是一个自定义的可下拉刷新的基于PullToRefreshBase的view,然后TabLayout就是上面说的要一直在顶部的导航栏,默认是隐藏。

MyPullRefreshScrollView:

public class MyPullRefreshScrollView extends PullToRefreshBase <NestedScrollView>{

    private NestedScrollView berScrollView;
    private FrameLayout flContent;

    public PullRefreshBerScrollView(Context context) {
        super(context);
    }

    public PullRefreshBerScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public PullRefreshBerScrollView(Context context, Mode mode) {
        super(context, mode);
    }

    @Override
    public Orientation getPullToRefreshScrollDirection() {
        return Orientation.VERTICAL;
    }

    @Override
    protected NestedScrollView createRefreshableView(Context context, AttributeSet attrs) {
        berScrollView = (NestedScrollView) LayoutInflater.from(context).inflate(R.layout.layout_berscrollview,null);
        flContent = (FrameLayout) berScrollView.findViewById(R.id.fl_content);
        return berScrollView;
    }

    public void addView(View view){
        flContent.addView(view);
    }

    public NestedScrollView getBerScrollView() {
        return berScrollView;
    }

    @Override
    protected boolean isReadyForPullEnd() {
        return false;
    }

    @Override
    protected boolean isReadyForPullStart() {
        return berScrollView.getScrollY() <= 0;
    }
}

下拉控件中,控制能否下拉的条件就是.getScrollY() <= 0(滑动距离是否小于等于0)

主要布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:background="@color/white"
        android:id="@+id/ll_scroll_content"
        ></LinearLayout>


    <android.support.design.widget.TabLayout
        android:id="@+id/tl_tab"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/white"
        app:tabMode="fixed"
        app:tabGravity="fill"
        app:tabTextColor="@color/app_black"
        app:tabSelectedTextColor="@color/login_red"
        app:tabIndicatorColor="@color/login_red"
        app:tabIndicatorHeight="2dp"
        app:tabTextAppearance="@style/MyTabLayoutTextAppearanceInverse"
        />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/divider_grey"/>


    <!--<com.xxx.xxx.ui.activity.test.MyTestViewPager-->
        <!--android:layout_width="match_parent"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:id="@+id/vp"-->
        <!--></com.xxx.xxx.ui.activity.test.MyTestViewPager>-->

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

</LinearLayout>

我用了mvvm模式,最上边的LinearLayout是用来动态添加View(本人不喜欢写死xml布局,这样扩展性差),TabLayout就是导航栏,下面我注释viewpager是因为我之前用viewpager,太麻烦了所以改用FragmentManager,所以这里用FrameLayout

2.初始化tablayout

我上面也说了,写一个帮助类来做tablayout间联动的操作,所以我这里就贴调用这歌辅助类的代码。

private void initTab(){
        tabGroup = new ProductDetailsTabGroup(this);
        tabGroup.addTabLayout(tabLayout);
        tabGroup.addTabLayout(topTabLayout);
        tabGroup.addTitiles(titles);
    }

监听滑动

scrollview.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
            @Override
            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                if (scrollY >= tabLayout.getTop()+contentView.getTop()+emptyViewGroup.getContentView().getTop()){
                    topTabLayout.setVisibility(View.VISIBLE);
                }else {
                    topTabLayout.setVisibility(View.GONE);
                }
            }
        });
3.设置fragmentManger
public void showFragment(int position){
        for (int i = 0; i < fragments.length; i++) {
            if (i == position){
                if (fragments[i] == null){
                    addFragment(position);
                    fragmentManager.beginTransaction().add(R.id.fl_child_content, fragments[i]).commit();
                }else {
                    fragmentManager.beginTransaction().attach(fragments[i]).commit();
                }
            }else {
                if (fragments[i] != null){
                    fragmentManager.beginTransaction().detach(fragments[i]).commit();
                }
            }
        }
    }
4.子view布局
<android.support.v4.widget.NestedScrollView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

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

    </android.support.v4.widget.NestedScrollView>

记得子view要嵌套NestedScrollView。

注意一下,如果你用RecyclerView做子View的话会产生滑动无惯性,这时候你需要给RecyclerView设一个属性recyclerview.setNestedScrollingEnabled(false);在xml中设也行,这样就正常了。

这样就能实现那个效果了,代码也不是很难,就是要多注意一些细节,而且使用FragmentManager的话连懒加载都不用做了,简直方便了很多。

5.总结

按照我这样的做法,你肯定能实现文章里gif图的那种效果,但是,这种方法是投机取巧的方法,也行不会有什么问题,但是和理论对不上,理论上实现这样的效果就是一种解决嵌套滑动的思路(NestedScrollView的那种思路才是正常解决这个方法的正确思路),我这样做虽然能实现,但是容易出BUG,扩展性不好。

再有,这样的情况,真的不使用viewpager,这里用viewpager只会把一个简单的问题给复杂化。

最后,我之前写过一篇关于NestedScrollView嵌套解决滑动冲突,这是我目前发现的能解决滑动冲突最好的方法,至于要实现折叠的特效,还是需要用CoordinatorLayout,而这个东西的卡顿BUG我估计这辈子谷歌是不会去解决它了,所以想做特效,我觉得要理解CoordinatorLayout封装的思想和自定义Behavior,或者直接自定义CoordinatorLayout进行扩展。


2017.11.13 更新

更新内容:添加demo
项目地址 : https://github.com/994866755/handsomeYe.productdetails

最近一直没怎么又时间更新,而且也发现github很久没维护了,然后也抽出点时间也写一个简单的demo实现这个商品详情页面的功能。希望有Bug的话可以提出,有写得不好的地方也能指出来,谢谢。

目录
相关文章
|
27天前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。
|
27天前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
97 1
|
4天前
|
编解码 Java Android开发
通义灵码:在安卓开发中提升工作效率的真实应用案例
本文介绍了通义灵码在安卓开发中的应用。作为一名97年的聋人开发者,我在2024年Google Gemma竞赛中获得了冠军,拿下了很多项目竞赛奖励,通义灵码成为我的得力助手。文章详细展示了如何安装通义灵码插件,并通过多个实例说明其在适配国际语言、多种分辨率、业务逻辑开发和编程语言转换等方面的应用,显著提高了开发效率和准确性。
|
3天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
14 5
|
1天前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
2天前
|
缓存 数据库 Android开发
安卓开发中的性能优化技巧
【10月更文挑战第29天】在移动应用的海洋中,性能是船只能否破浪前行的关键。本文将深入探讨安卓开发中的性能优化策略,从代码层面到系统层面,揭示如何让应用运行得更快、更流畅。我们将以实际案例和最佳实践为灯塔,引领开发者避开性能瓶颈的暗礁。
11 3
|
5天前
|
存储 IDE 开发工具
探索Android开发之旅:从新手到专家
【10月更文挑战第26天】在这篇文章中,我们将一起踏上一段激动人心的旅程,探索如何在Android平台上从零开始,最终成为一名熟练的开发者。通过简单易懂的语言和实际代码示例,本文将引导你了解Android开发的基础知识、关键概念以及如何实现一个基本的应用程序。无论你是编程新手还是希望扩展你的技术栈,这篇文章都将为你提供价值和启发。让我们开始吧!
|
28天前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
57 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
11天前
|
Java API Android开发
安卓应用程序开发的新手指南:从零开始构建你的第一个应用
【10月更文挑战第20天】在这个数字技术不断进步的时代,掌握移动应用开发技能无疑打开了一扇通往创新世界的大门。对于初学者来说,了解并学习如何从无到有构建一个安卓应用是至关重要的第一步。本文将为你提供一份详尽的入门指南,帮助你理解安卓开发的基础知识,并通过实际示例引导你完成第一个简单的应用项目。无论你是编程新手还是希望扩展你的技能集,这份指南都将是你宝贵的资源。
39 5
|
9天前
|
设计模式 IDE Java
探索安卓开发:从新手到专家的旅程
【10月更文挑战第22天】 在数字时代的浪潮中,移动应用开发如同一座金矿,吸引着无数探险者。本文将作为你的指南针,指引你进入安卓开发的广阔天地。我们将一起揭开安卓平台的神秘面纱,从搭建开发环境到掌握核心概念,再到深入理解安卓架构。无论你是初涉编程的新手,还是渴望进阶的开发者,这段旅程都将为你带来宝贵的知识和经验的财富。让我们开始吧!