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

相关文章

基于 HTML5 Canvas 实现图片旋转与下载功能(完整代码展示)

《基于HTML5Canvas实现图片旋转与下载功能(完整代码展示)》本文将深入剖析一段基于HTML5Canvas的代码,该代码实现了图片的旋转(90度和180度)以及旋转后图片的下载... 目录一、引言二、html 结构分析三、css 样式分析四、JavaScript 功能实现一、引言在 Web 开发中,

SpringBoot中使用Flux实现流式返回的方法小结

《SpringBoot中使用Flux实现流式返回的方法小结》文章介绍流式返回(StreamingResponse)在SpringBoot中通过Flux实现,优势包括提升用户体验、降低内存消耗、支持长连... 目录背景流式返回的核心概念与优势1. 提升用户体验2. 降低内存消耗3. 支持长连接与实时通信在Sp

Conda虚拟环境的复制和迁移的四种方法实现

《Conda虚拟环境的复制和迁移的四种方法实现》本文主要介绍了Conda虚拟环境的复制和迁移的四种方法实现,包括requirements.txt,environment.yml,conda-pack,... 目录在本机复制Conda虚拟环境相同操作系统之间复制环境方法一:requirements.txt方法

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

Nginx 配置跨域的实现及常见问题解决

《Nginx配置跨域的实现及常见问题解决》本文主要介绍了Nginx配置跨域的实现及常见问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来... 目录1. 跨域1.1 同源策略1.2 跨域资源共享(CORS)2. Nginx 配置跨域的场景2.1

Python中提取文件名扩展名的多种方法实现

《Python中提取文件名扩展名的多种方法实现》在Python编程中,经常会遇到需要从文件名中提取扩展名的场景,Python提供了多种方法来实现这一功能,不同方法适用于不同的场景和需求,包括os.pa... 目录技术背景实现步骤方法一:使用os.path.splitext方法二:使用pathlib模块方法三

CSS实现元素撑满剩余空间的五种方法

《CSS实现元素撑满剩余空间的五种方法》在日常开发中,我们经常需要让某个元素占据容器的剩余空间,本文将介绍5种不同的方法来实现这个需求,并分析各种方法的优缺点,感兴趣的朋友一起看看吧... css实现元素撑满剩余空间的5种方法 在日常开发中,我们经常需要让某个元素占据容器的剩余空间。这是一个常见的布局需求

HTML5 getUserMedia API网页录音实现指南示例小结

《HTML5getUserMediaAPI网页录音实现指南示例小结》本教程将指导你如何利用这一API,结合WebAudioAPI,实现网页录音功能,从获取音频流到处理和保存录音,整个过程将逐步... 目录1. html5 getUserMedia API简介1.1 API概念与历史1.2 功能与优势1.3

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja