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

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、