仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

一、 需求介绍

因为业务模块的扩张,导致各类业务入口疯狂增加,原先的入口UI样式已经满足不了产品经理的需求了,所以他们提出了能不能仿照淘宝的业务导航栏给我们自己的产品做一个优化,废话不多说,原先他们的需求大致是这样的↓

仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

最后经过产品和老板们的商讨,最终一致决定采用如下样式

仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

可以看到,这个和淘宝的区别在于:

  1. 我们默认显示 4 个图标,并且有左右两个“箭头”,当ScrollView不能继续向左滑动时,左箭头隐藏,反之不能继续向右滑动时,右箭头隐藏。其他状态 “箭头”都显示。
  2. 用户可以选择点击“箭头”来实现滑动,也可以用手指左右拖动来进行滑动。当用户通过点击“箭头”滑动时,点击一次滑动4个图标,不能有偏差
  3. 我们只有一行图标需要滑动,而淘宝是两行。
  4. 此外我们还有个额外需求,当图标露出超过五分之四时,则把此图标视为可见状态,此时当用户点击”箭头“滑动时,就需要跳过此图标(也就是直接向后滑动4个)。反之图标为不可见状态时,用户点击”箭头“滑动时,则不能跳过此图标(也就是向后滑动3个图标)。例如:下图的 游乐园 图标,此时它被 ”左箭头“给遮挡了,但是超过五分之四的部分显示出来了,则视为可见状态,当用户点击 ”左箭头“ 进行滑动时,则跳过此图标,直接向后滑动4个图标。并且通过 ”箭头“ 滑动后的图标不能处于被遮挡状态,都需要完整的进行显示。
仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

看到了大致需求后,我们来做一下技术分析,首先映入眼帘的是一个可左右滚动的View,这个第一时间想到可以用 HorizontalScrollView 来实现,但仔细一看底部有一个小进度条,这个在 HorizontalScrollView 是没有现成的可以使用的,所以只能自己去自定义一个了。最后还有就是实现左右“箭头”的点击滑动。

仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

所以,我们这个需求需要拆分成以下几步来完成:

  1. 引入HorizontalScrollView,把图标塞进这个HorizontalScrollView中进行展示
  2. 自定义底部进度条(BottomProgressBar)
  3. 加入可点击的左右“箭头”,并实现点击后自动滑动4个图标的功能

二、 代码实现

本 Demo 的代码我会放在GitHub上,有需要的看官可以自行去Clone,但请别忘了点赞哦。

1. 引入 HorizontalScrollView

在布局文件 activity_main.xml 中,我们引入HorizontalScrollView, 代码如下:

<?xml version="1.0" encoding="utf-8"?>


    
    

        
    



其中需要注意 include 标签,其作用是可以将一个指定布局文件加载到当前布局文件中,其中的 entry_layout.xml布局文件里放了13个图标,使用include标签可以使得主布局文件看起来不会显得很冗长,方便直观的看出布局结构。

布局文件添加好后,就需要把这13个入口图标全都塞进去,并且在MainActivity里引入布局,然后就会得到这样的一个效果

仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

2. 自定义底部进度条(BottomProgressBar)

这个进度条布局非常简单,就一个白色底板和一个蓝色进度,所以我们先给他定义一个布局如下:

<?xml version="1.0" encoding="utf-8"?>


    



其中的android:background属性里的 @drawable/entry_progress_bar_background@drawable/entry_progress都是经过圆角处理后的矩形,这里只放一个代码了,有需要的可以直接去GitHub上自取

<?xml version="1.0" encoding="utf-8"?>

    
        
            
            
        
    


然后在EntryProgressBar.java中引入此布局,并定义一个可以更新进度的方法setProgress() 具体代码如下:

package com.example.entry_demo.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;

import androidx.annotation.Nullable;

import com.example.entry_demo.R;
import com.example.entry_demo.util.ViewHelper;

/**
 * 次入口的蓝色小滚动条
 */
public class EntryProgressBar extends LinearLayout {
    View progress_view;
    private float currentX = 0;
    //每一页展示的icon个数
    private float display_count = 4;
    //icon的总个数
    private float total_count = 5;
    //进度条组件总宽度32dp
    private float width_dp = 32;
    //蓝色进度的宽度dp
    private float blue_width_dp = 20;

    public EntryProgressBar(Context context) {
        this(context, null);
    }

    public EntryProgressBar(Context context,@Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context) {
        // 引入布局
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.entry_progress_bar_layout, this);
        progress_view = this.findViewById(R.id.progress_view);
        setBlueWidth();
    }

    //设置蓝色进度的width
    private void setBlueWidth() {
        blue_width_dp = (display_count / total_count) * width_dp;
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                ViewHelper.dp2px(getContext(),blue_width_dp), LayoutParams.MATCH_PARENT);
        progress_view.setLayoutParams(params);
    }

    // 设置总的入口个数
    public void setTotalCount(int total_count) {
        this.total_count = total_count;
        setBlueWidth();
    }

    // 更新进度
    public void setProgress(float percent_progress) {
        float progress = 0;
        progress = ViewHelper.dp2px(getContext(), width_dp - blue_width_dp) * percent_progress;
        // 开启动画
        TranslateAnimation animation = new TranslateAnimation(currentX, progress, 0, 0);
        animation.setDuration(200);
        // 设置动画过后不复位
        animation.setFillEnabled(true);
        animation.setFillAfter(true);

        progress_view.startAnimation(animation);
        currentX = progress;
    }
}

最后在 MianActivity 的onCreate()方法中给HorizontalView添加一个滚动监听,并且在监听中时刻更新 EntryProgressBar的进度,代码如下:

// 给HorizontalScrollView 添加滑动监听
private void addListenerToHorizontalView() {
    // 使用post方法是因为,在构建入口时,需要先获取View的宽度,然而只有当View进行到onLayout阶段后,才能得到准群的width,所以才调用view.post方法来保证获取到width是准确的
    horizontalScrollView.post(new Runnable() {
        @Override
        public void run() {
            horizontalScrollView_width_dp = ViewHelper.px2dip(context, horizontalScrollView.getWidth());
        }
    });

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        horizontalScrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
            @Override
            public void onScrollChange(View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                float left_btn_alpha = 1.0f;
                float right_btn_alpha = 1.0f;

                // 获取可滑动范围
                int scrollRange = 0;
                int scroll_X = horizontalScrollView.getScrollX();
                if (horizontalScrollView.getChildCount() > 0) {
                    View child = horizontalScrollView.getChildAt(0);
                    scrollRange = Math.max(0, child.getWidth() - (horizontalScrollView.getWidth() - (horizontalScrollView.getPaddingLeft() + horizontalScrollView.getPaddingRight())));
                }

                if (scrollRange > 0 ) {
                    // 同步更新底部蓝色进度条
                    entry_progress_bar.setProgress((float) scroll_X / scrollRange);

                    //设置左右按钮的渐变透明度
                    if (scroll_X >= scrollRange - ViewHelper.dp2px(context, 40)) {
                        left_btn_alpha = (float) (scrollRange - scroll_X) / ViewHelper.dp2px(context, 40);
                    } else if (scroll_X < ViewHelper.dp2px(context, 40)) {
                        right_btn_alpha = (float) scroll_X / ViewHelper.dp2px(context, 40);
                    }
                }
            }
        });
    }
}

到此为止,我们得到了这样的一个效果,可以看到进度条已经制作完成了:

仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

3. 加入可点击的左右“箭头”,并实现点击滑动功能

因为老板们给的需求是,默认只显示4个图标,但现在显示了5个,所以在实现”箭头“点击功能之前,我们还需要修改下每个图标之间的间距,保证在不同分辨率下,都只显示出4个图标,并且此时蓝色进度条的滚动不是很明显,我们也需要调用setTotalCount()来让滚动变得更加容易区分,具体代码如下:

    private void initEntry() {
        List entriesTemp = Arrays.asList(abs_1, cbs_2, ctbs_3, destination_4, gbs_5, home_fbs_6, lbs_7, nlbs_8, pbs_9, rbs_10, staycation_11, tbs_12, frbs_13);
        // 根据屏幕大小计算icon之间的间距
        int entrySize = entriesTemp.size();
        if (entrySize > 4) {
            // 之所以减去64dp,是因为右滑按钮占40dp,marginStar占24dp
            entry_margin_dp = (horizontalScrollView_width_dp - icon_width_dp * 4 - 64) / 4;
            // 设置图标总数
            entry_progress_bar.setTotalCount(entrySize);
        } else {
            // 之所以减去48dp,是因为当入口小于4个时,左右按钮隐藏,但marginStar和marginEnd各占24dp
            entry_margin_dp = (horizontalScrollView_width_dp - icon_width_dp * 4 -48) / 3;
            entry_progress_bar.setVisibility(View.GONE);
        }

        for (int i = 0; i < entriesTemp.size(); i ++) {
            EntryView entry = entriesTemp.get(i);
            int margin = entry_margin_dp;
            if (i < entrySize) {
                if (i == entrySize - 1) {
                    //设置最后一个入口的margin
                    margin = 24;
                }
                ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) entry.getLayoutParams();
                if (i == 0) {
                    params.setMarginStart(ViewHelper.dp2px(context, 24));
                }
                params.setMarginEnd(ViewHelper.dp2px(context, margin));
                entry.setLayoutParams(params);
            } else {
                entry.setVisibility(View.GONE);
            }
        }
    }

然后添加左右”箭头“比较简单,直接在activity_main.xml中添加既可,但是难点在于如何通过点击它,稳定的进行滑动,并且满足额外需求

此外我们还有个额外需求,当图标露出超过五分之四时,则把此图标视为可见状态,此时当用户点击”箭头“滑动时,就需要跳过此图标(也就是直接向后滑动4个)。反之图标为不可见状态时,用户点击”箭头“滑动时,则不能跳过此图标(也就是向后滑动3个图标)。例如:下图的 游乐园 图标,此时它被 ”左箭头“给遮挡了,但是超过五分之四的部分显示出来了,则视为可见状态,当用户点击 ”左箭头“ 进行滑动时,则跳过此图标,直接向后滑动4个图标。并且通过 ”箭头“ 滑动后的图标不能处于被遮挡状态,都需要完整的进行显示。

这个问题的处理其实就相当于一个小算法实践了,我的处理是用一个List去记录下每个图标滑动了多少dp后达到可见与不可见时的分界线,例如:对于第一个图标而言,由于图标大小是60dp,当我向左滑动60dp后,它就被完全遮挡了,所以这个60就是图标1的索引坐标,当然其中还有很多细节,有需求的看官可以看如下代码:


//设置索引坐标,用于判断次入口是否处于显示状态
private List visible_index = new ArrayList();
...
...
...
// 初始化索引坐标
int index = 0;
visible_index.add(0, 0);
for (int i = 1; i <= entrySize - 4; i++) {
    // 注意,entrySize - 4 是因为默认每一页显示4个图标,当HorizontalView不可滑动时,这4个图标一定是处于可见状态的,所以不用加入到索引坐标中
    // 第一个和最后一个可见索引坐标为60dp,其他均为60dp + secondaryEntry_margin_dp
    if (i == 1 || i == entrySize - 4) {
        index += icon_width_dp;
    } else {
        index += (icon_width_dp + entry_margin_dp);
    }
    visible_index.add(i, index);
}

最后就是点击”箭头“以后滑动4个的功能,由于我们图标大小固定(IconWidth = 60dp),图标与图标之间的间隔(entry_margin_dp)也是固定的,所以我们只要向左或者向右滑动(IconWidth + entry_margin_dp) * 4 就是一次滑动4个的功能了,代码我就不在这贴了,还是那句话,有需要的看官可以直接去看下源码。

最后的最后,我们得到了这样完成了这个需求。

人为手动左右滑动效果如下:

仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

用户点击”箭头“后的滑动效果如下:

仿Android淘宝顶部导航栏的HorizontalScrollView Demo实例

总结

之所以把这个需求拿出来做个demo分享,是因为里面涉及许多常见面试内容和基础,例如:自定义View的使用、常用布局和布局优化、不同手机分辨率下如何保证布局的一致性等。

代码里也有许多细节我通过注释的形式记录了下来,在文章中就没有一一列举了。

// 使用post方法是因为,在构建入口时,需要先获取View的宽度,然而只有当View进行到onLayout阶段后,才能得到准群的width,所以才调用view.post方法来保证获取到width是准确的
horizontalScrollView.post(new Runnable() {
    @Override
    public void run() {
        horizontalScrollView_width_dp = ViewHelper.px2dip(context, horizontalScrollView.getWidth());
        initEntry();
    }
});

最后如果你在某一方面发现了bug或者有更好的方法可以替代,欢迎和我进行分享,一起进步,最后还愣着干嘛,点个赞吧!

作者:Android王小波
链接:https://juejin.cn/post/7114578467626156046

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章