Android-仿网易云歌手资料页面的实现-NestedScrolling

2023-10-11 12:50

本文主要是介绍Android-仿网易云歌手资料页面的实现-NestedScrolling,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、简介

先来看看效果图:

按照上图:

按照传统的事件分发去理解,我们滑动的是下面的内容区域,而移动的却是外部的ViewGroup,如果采用传统的事件分发,是外部的Parent拦截了(Parent的onInterceptTouchEvent返回true)内部的Child的事件,但是,上面的效果中,当Parent滑动到一定的距离时,Child又开始滑动,整个过程是同一个事件序列。传统的事件分发中,当Parent拦截了事件后(Parent的onInterceptTouchEvent返回true),是无法再把事件交给Child的。

注意:某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话)并且它的onInterceptTouchEvent不会再被调用。

但是NestedScrolling机制来处理这个事情就很好办,不了解的可以先了解一下再回来。

NestedScrolling 推荐这篇文章:https://www.jianshu.com/p/f09762df81a5

接下来上代码,首先是布局文件:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><ImageViewandroid:id="@+id/id_stickynavlayout_avatar"android:layout_width="match_parent"android:layout_height="220dp"android:src="@drawable/taylor_swift"android:scaleType="centerCrop"/><com.example.hp.android_stickynavlayout.custom.StickNavLayoutandroid:id="@+id/id_stickynavlayout"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:fillViewport="true"><com.example.hp.android_stickynavlayout.custom.SimpleViewPagerIndicatorandroid:id="@+id/id_stickynavlayout_indicator"android:layout_width="match_parent"android:layout_height="40dp"android:layout_marginTop="220dp"android:background="@android:color/white"></com.example.hp.android_stickynavlayout.custom.SimpleViewPagerIndicator><android.support.v4.view.ViewPagerandroid:id="@+id/id_stickynavlayout_viewpager"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_toEndOf="@id/id_stickynavlayout_indicator"android:layout_toRightOf="@id/id_stickynavlayout_indicator"></android.support.v4.view.ViewPager></com.example.hp.android_stickynavlayout.custom.StickNavLayout><include layout="@layout/online_search_bar"/></RelativeLayout>

最外层是RelativeLayout,然后是顶部图片,然后是我们的自定义的控件StickyNavLayout,注意它的宽高都是match_parent,然后是Vp的指示器(SimpleViewPagerIndicator),最后是ViewPager。

注意这里StickyNavLayout 在顶部图片的上层,要为顶部图片留出空, SimpleViewPagerIndicator 设置了marginTop。

还有 ViewPager 的父布局 StickyNavLayout 要添加 android:fillViewport="true" ,否则Viewpager无法显示。这是因为在ViewPager想要match_Parent而高度不够时,需要在父布局加上这个属性,否则没有效果。

接下来是MainActivity:

public class MainActivity extends AppCompatActivity implements SimpleViewPagerIndicator.IndicatorClickListener, StickNavLayout.MyStickyListener{public static final String UID = "UID";public static final String[] titles = new String[]{"单曲","专辑","MV","歌手信息"};@Bind(R.id.id_stickynavlayout)StickNavLayout mStickNavLayout;@Bind(R.id.id_stickynavlayout_avatar)ImageView iv_avatar;@Bind(R.id.id_stickynavlayout_indicator)SimpleViewPagerIndicator mIndicator;@Bind(R.id.id_stickynavlayout_viewpager)ViewPager mViewPager;private TabFragmentPagerAdapter mAdapter;private List<Fragment> mFragments = new ArrayList<>();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ButterKnife.bind(this);if(Build.VERSION.SDK_INT >= 21){View decorView = getWindow().getDecorView();//int option = View.SYSTEM_UI_FLAG_VISIBLE|View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE|View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;decorView.setSystemUiVisibility(option);
//            getWindow().setStatusBarColor(Color.parseColor("#9C27B0"));getWindow().setStatusBarColor(Color.TRANSPARENT);}initView();initData();}protected void initData() {}protected void initView() {mIndicator.setIndicatorClickListener(this);mIndicator.setTitles(titles);mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {mIndicator.scroll(position,positionOffset);}@Overridepublic void onPageSelected(int position) {}@Overridepublic void onPageScrollStateChanged(int state) {}});for(int i=0;i<titles.length;i++){mFragments.add(ADetailSongFragment.newInstance());}mAdapter = new TabFragmentPagerAdapter(getSupportFragmentManager(),mFragments);mViewPager.setAdapter(mAdapter);mViewPager.setCurrentItem(0);mStickNavLayout.setScrollListener(this);int height = DisplayUtil.getScreenHeight(MainActivity.this)-DisplayUtil.dip2px(MainActivity.this,65)-DisplayUtil.dip2px(MainActivity.this,40);LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) mViewPager.getLayoutParams();layoutParams.height = height;mViewPager.setLayoutParams(layoutParams);}public static void toArtistDetailActivity(Context context, String uid){Intent intent = new Intent(context,MainActivity.class);intent.putExtra(UID,uid);context.startActivity(intent);}@Overridepublic void onClickItem(int k) {mViewPager.setCurrentItem(k);}//获取手机屏幕宽度,像素为单位private float getMobileWidth() {DisplayMetrics dm = new DisplayMetrics();getWindowManager().getDefaultDisplay().getMetrics(dm);int width = dm.widthPixels;return width;}//改变顶部图片的大小,参数为导航栏相对于其父布局的top@Overridepublic void imageScale(float bottom) {float height = DisplayUtil.dip2px(MainActivity.this,220);float mScale = bottom/height;float width = getMobileWidth()*mScale;float dx = (width-getMobileWidth())/2;iv_avatar.layout((int)(0-dx),0,(int)(getMobileWidth()+dx),(int)bottom);}
}

注意在 initView 中,为 ViewPager动态设置了高度,因为在列表加载出来之前,我们并不知道viewpager的高度是多少,如果设置为match_parent,则viewpager的高度被固定为屏幕剩下的高度,那么在往上滑动时viewpager就出现无法填满整个屏幕的情况,如果设置为wrap_content,则会导致ViewPager里面的RecyclerView显示不全,读者可以试试。

fragment的代码就不贴了,只有一个recyclerView列表

二、StickyNavLayout解析

1、代码如下

public class StickNavLayout extends LinearLayout implements NestedScrollingParent {public static final String TAG = "StickNavLayout";private View mNav;private ViewPager mViewPager;private ValueAnimator mOffsetAnimator;private Interpolator mInterpolator;private MyStickyListener listener;//scroll表示手指滑动多少距离,界面跟着显示多少距离,而fling是根据你的滑动方向与轻重,还会自动滑动一段距离。public StickNavLayout(Context context, @Nullable AttributeSet attrs) {super(context, attrs);setOrientation(LinearLayout.VERTICAL);}@Overrideprotected void onFinishInflate() {super.onFinishInflate();mNav = findViewById(R.id.id_stickynavlayout_indicator);View view = findViewById(R.id.id_stickynavlayout_viewpager);if(!(view instanceof ViewPager)){throw new RuntimeException("id_stickynavlayout_viewpager should used by ViewPager!");}mViewPager = (ViewPager) view;}/*** 只有在onStartNestedScroll返回true的时候才会接着调用onNestedScrollAccepted,* 这个判断是需要我们自己来处理的,* 不是直接的父子关系一样可以正常进行*/@Overridepublic boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {Log.e(TAG, "onStartNestedScroll");return true;}/*** 字面意思可以理解出来父View接受了子View的邀请,可以在此方法中做一些初始化的操作。*/@Overridepublic void onNestedScrollAccepted(View child, View target, int axes) {Log.e(TAG, "onNestedScrollAccepted");}/*** 每次子View在滑动前都需要将滑动细节传递给父View,* 一般情况下是在ACTION_MOVE中调用* public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow),* dispatchNestedPreScroll在ScrollView、ListView的Action_Move中被调用* 然后父View就会被回调public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)。*/private int mNavTop = -1;private int mViewPagerTop = -1;@Overridepublic void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {Log.e(TAG, "onNestedPreScroll is call");//dy:鼠标往上走是正,往下走是负//方法一if(mNavTop == -1){mNavTop = mNav.getTop();}if(mViewPagerTop == -1){mViewPagerTop = mViewPager.getTop();}int moveY = (int) Math.sqrt(Math.abs(dy)*2);if(dy < 0){//往下拉if(getScrollY() == 0 && mNav.getTop() >= mNavTop) {mNav.layout(mNav.getLeft(), mNav.getTop() + moveY, mNav.getRight(), mNav.getBottom() + moveY);mViewPager.layout(mViewPager.getLeft(), mViewPager.getTop() + moveY, mViewPager.getRight(), mViewPager.getBottom() + moveY);listener.imageScale(mNav.getTop());consumed[1] = dy;}else if(getScrollY() > 0 && !ViewCompat.canScrollVertically(target,-1)){if(getScrollY()+dy<0){scrollTo(0,0);}else {scrollTo(0, getScrollY() + dy);consumed[1] = dy;}}}else if(dy > 0){if(mNav.getTop() > mNavTop){if(mNav.getTop()-moveY < mNavTop){mNav.layout(mNav.getLeft(),mNavTop,mNav.getRight(),mNavTop+mNav.getHeight());mViewPager.layout(mViewPager.getLeft(),mViewPagerTop,mViewPager.getRight(),mViewPagerTop+mViewPager.getHeight());listener.imageScale(mNavTop);consumed[1] = dy;}else {mNav.layout(mNav.getLeft(), mNav.getTop() - moveY, mNav.getRight(), mNav.getBottom() - moveY);mViewPager.layout(mViewPager.getLeft(), mViewPager.getTop() - moveY, mViewPager.getRight(), mViewPager.getBottom() - moveY);listener.imageScale(mNav.getTop());consumed[1] = dy;}}else if(getScrollY()<DisplayUtil.dip2px(getContext(),155)){if(getScrollY()+dy>DisplayUtil.dip2px(getContext(),155)){scrollTo(0,DisplayUtil.dip2px(getContext(),155));consumed[1] = dy;}else {scrollTo(0, getScrollY() + dy);consumed[1] = dy;}}}}/*** 接下来子View就要进自己的滑动操作了,滑动完成后子View还需要调用* public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)* 将自己的滑动结果再次传递给父View,父View对应的会被回调* public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed),* 但这步操作有一个前提,就是父View没有将滑动值全部消耗掉,因为父View全部消耗掉,子View就不应该再进行滑动了* 子View进行自己的滑动操作时也是可以不全部消耗掉这些滑动值的,剩余的可以再次传递给父View,* 使父View在子View滑动结束后还可以根据子View剩余的值再次执行某些操作。*/@Overridepublic void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}/*** ACTION_UP或者ACTION_CANCEL的到来,* 子View需要调用public void stopNestedScroll()来告知父View本次NestedScrollig结束,* 父View对应的会被回调public void onStopNestedScroll(View target),*/@Overridepublic void onStopNestedScroll(View child) {if(mNav.getTop() != mNavTop) {mNav.layout(mNav.getLeft(),mNavTop,mNav.getRight(),mNavTop+mNav.getHeight());mViewPager.layout(mViewPager.getLeft(),mViewPagerTop,mViewPager.getRight(),mViewPagerTop+mViewPager.getHeight());listener.imageScale(mNavTop);}}@Overridepublic boolean onNestedPreFling(View target, float velocityX, float velocityY) {return false;}@Overridepublic boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {//鼠标向下拉,velocityY为负if(target instanceof RecyclerView && velocityY < 0){final RecyclerView recyclerView = (RecyclerView) target;final View firstChild = recyclerView.getChildAt(0);final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);consumed = childAdapterPosition > 3;}if(!consumed){animateScroll(velocityY,computeDuration(0),consumed);}else{animateScroll(velocityY,computeDuration(velocityY),consumed);}return true;}private int computeDuration(float velocityY) {final int distance;if(velocityY > 0){//鼠标往上distance = Math.abs(mNav.getTop() - getScrollY());}else{//鼠标往下distance = Math.abs(getScrollY());}final int duration;velocityY = Math.abs(velocityY);if(velocityY > 0){duration = 3 * Math.round(1000 * (distance / velocityY));}else{final float distanceRadtio = distance/getHeight();duration = (int) ((distanceRadtio+1)*150);}return duration;}private void animateScroll(float velocityY, int duration, boolean consumed) {final int currentOffset = getScrollY();final int topHeight = mNav.getTop();if(mOffsetAnimator == null){mOffsetAnimator = new ValueAnimator();mOffsetAnimator.setInterpolator(mInterpolator);mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {if(animation.getAnimatedValue() instanceof Integer){scrollTo(0, (Integer) animation.getAnimatedValue());}}});}else{mOffsetAnimator.cancel();}mOffsetAnimator.setDuration(Math.min(duration,600));if(velocityY >= 0){mOffsetAnimator.setIntValues(currentOffset,mNav.getTop()-DisplayUtil.dip2px(getContext(),65));mOffsetAnimator.start();}else{if(!consumed){mOffsetAnimator.setIntValues(currentOffset,0);mOffsetAnimator.start();}}}@Overridepublic int getNestedScrollAxes() {return 0;}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {switch (ev.getAction()){case MotionEvent.ACTION_UP:if(mNav.getTop()>mNavTop){return true;}break;}return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_UP:mNav.layout(mNav.getLeft(),mNavTop,mNav.getRight(),mNavTop+mNav.getHeight());break;}return super.onTouchEvent(event);}public void setScrollListener(MyStickyListener myOnScrollListener){this.listener = myOnScrollListener;}public interface MyStickyListener{void imageScale(float v);}
}

继承自LinearLayout,实现NestedScrollingParent接口,NestedScrollingParent 的方法就是你要做的事情,方法也加了注释,这个没什么好讲的。。。

这里通过回调方法改变图片的大小,通过layout进行布局的调整

注意这里NestedScrollingParent 接口的方法,参数dy等和平时使用的dy有所不同,比如

onNestedPreScroll中按住,鼠标往上拉时,dy为正,鼠标往下拉时,dy为负。

完整项目地址:https://github.com/wuxiaogui593/AndroidStickyNavLayout

有什么错误或问题欢迎骚扰!!!

这篇关于Android-仿网易云歌手资料页面的实现-NestedScrolling的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/188151

相关文章

关于集合与数组转换实现方法

《关于集合与数组转换实现方法》:本文主要介绍关于集合与数组转换实现方法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、Arrays.asList()1.1、方法作用1.2、内部实现1.3、修改元素的影响1.4、注意事项2、list.toArray()2.1、方

使用Python实现可恢复式多线程下载器

《使用Python实现可恢复式多线程下载器》在数字时代,大文件下载已成为日常操作,本文将手把手教你用Python打造专业级下载器,实现断点续传,多线程加速,速度限制等功能,感兴趣的小伙伴可以了解下... 目录一、智能续传:从崩溃边缘抢救进度二、多线程加速:榨干网络带宽三、速度控制:做网络的好邻居四、终端交互

java实现docker镜像上传到harbor仓库的方式

《java实现docker镜像上传到harbor仓库的方式》:本文主要介绍java实现docker镜像上传到harbor仓库的方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 前 言2. 编写工具类2.1 引入依赖包2.2 使用当前服务器的docker环境推送镜像2.2

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Java easyExcel实现导入多sheet的Excel

《JavaeasyExcel实现导入多sheet的Excel》这篇文章主要为大家详细介绍了如何使用JavaeasyExcel实现导入多sheet的Excel,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录1.官网2.Excel样式3.代码1.官网easyExcel官网2.Excel样式3.代码

python实现对数据公钥加密与私钥解密

《python实现对数据公钥加密与私钥解密》这篇文章主要为大家详细介绍了如何使用python实现对数据公钥加密与私钥解密,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录公钥私钥的生成使用公钥加密使用私钥解密公钥私钥的生成这一部分,使用python生成公钥与私钥,然后保存在两个文

浏览器插件cursor实现自动注册、续杯的详细过程

《浏览器插件cursor实现自动注册、续杯的详细过程》Cursor简易注册助手脚本通过自动化邮箱填写和验证码获取流程,大大简化了Cursor的注册过程,它不仅提高了注册效率,还通过友好的用户界面和详细... 目录前言功能概述使用方法安装脚本使用流程邮箱输入页面验证码页面实战演示技术实现核心功能实现1. 随机

Golang如何对cron进行二次封装实现指定时间执行定时任务

《Golang如何对cron进行二次封装实现指定时间执行定时任务》:本文主要介绍Golang如何对cron进行二次封装实现指定时间执行定时任务问题,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录背景cron库下载代码示例【1】结构体定义【2】定时任务开启【3】使用示例【4】控制台输出总结背景

Golang如何用gorm实现分页的功能

《Golang如何用gorm实现分页的功能》:本文主要介绍Golang如何用gorm实现分页的功能方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录背景go库下载初始化数据【1】建表【2】插入数据【3】查看数据4、代码示例【1】gorm结构体定义【2】分页结构体

在Golang中实现定时任务的几种高效方法

《在Golang中实现定时任务的几种高效方法》本文将详细介绍在Golang中实现定时任务的几种高效方法,包括time包中的Ticker和Timer、第三方库cron的使用,以及基于channel和go... 目录背景介绍目的和范围预期读者文档结构概述术语表核心概念与联系故事引入核心概念解释核心概念之间的关系