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">
<ImageView
android: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.StickNavLayout
android: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.SimpleViewPagerIndicator
android: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.ViewPager
android: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无法显示。
接下来是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<>();
@Override
protected 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() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
mIndicator.scroll(position,positionOffset);
}
@Override
public void onPageSelected(int position) {
}
@Override
public 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);
}
@Override
public 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
@Override
public 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无法填满屏幕,如果设置为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);
}
@Override
protected 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,
* 这个判断是需要我们自己来处理的,
* 不是直接的父子关系一样可以正常进行
*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.e(TAG, "onStartNestedScroll");
return true;
}
/**
* 字面意思可以理解出来父View接受了子View的邀请,可以在此方法中做一些初始化的操作。
*/
@Override
public 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;
@Override
public 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剩余的值再次执行某些操作。
*/
@Override
public 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),
*/
@Override
public 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);
}
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public 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() {
@Override
public 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();
}
}
}
@Override
public int getNestedScrollAxes() {
return 0;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
if(mNav.getTop()>mNavTop){
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public 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
有什么错误或问题欢迎骚扰!!!