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

相关文章

Java根据IP地址实现归属地获取

《Java根据IP地址实现归属地获取》Ip2region是一个离线IP地址定位库和IP定位数据管理框架,这篇文章主要为大家详细介绍了Java如何使用Ip2region实现根据IP地址获取归属地,感兴趣... 目录一、使用Ip2region离线获取1、Ip2region简介2、导包3、下编程载xdb文件4、J

PyQt5+Python-docx实现一键生成测试报告

《PyQt5+Python-docx实现一键生成测试报告》作为一名测试工程师,你是否经历过手动填写测试报告的痛苦,本文将用Python的PyQt5和python-docx库,打造一款测试报告一键生成工... 目录引言工具功能亮点工具设计思路1. 界面设计:PyQt5实现数据输入2. 文档生成:python-

Android实现一键录屏功能(附源码)

《Android实现一键录屏功能(附源码)》在Android5.0及以上版本,系统提供了MediaProjectionAPI,允许应用在用户授权下录制屏幕内容并输出到视频文件,所以本文将基于此实现一个... 目录一、项目介绍二、相关技术与原理三、系统权限与用户授权四、项目架构与流程五、环境配置与依赖六、完整

浅析如何使用xstream实现javaBean与xml互转

《浅析如何使用xstream实现javaBean与xml互转》XStream是一个用于将Java对象与XML之间进行转换的库,它非常简单易用,下面将详细介绍如何使用XStream实现JavaBean与... 目录1. 引入依赖2. 定义 JavaBean3. JavaBean 转 XML4. XML 转 J

Android 12解决push framework.jar无法开机的方法小结

《Android12解决pushframework.jar无法开机的方法小结》:本文主要介绍在Android12中解决pushframework.jar无法开机的方法,包括编译指令、框架层和s... 目录1. android 编译指令1.1 framework层的编译指令1.2 替换framework.ja

Flutter实现文字镂空效果的详细步骤

《Flutter实现文字镂空效果的详细步骤》:本文主要介绍如何使用Flutter实现文字镂空效果,包括创建基础应用结构、实现自定义绘制器、构建UI界面以及实现颜色选择按钮等步骤,并详细解析了混合模... 目录引言实现原理开始实现步骤1:创建基础应用结构步骤2:创建主屏幕步骤3:实现自定义绘制器步骤4:构建U

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Android开发环境配置避坑指南

《Android开发环境配置避坑指南》本文主要介绍了Android开发环境配置过程中遇到的问题及解决方案,包括VPN注意事项、工具版本统一、Gerrit邮箱配置、Git拉取和提交代码、MergevsR... 目录网络环境:VPN 注意事项工具版本统一:android Studio & JDKGerrit的邮

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展