浅析NestedScrolling嵌套滑动机制之实践篇-仿写饿了么商家详情页

upAlphaGradientPro,
getContext().getResources().getColor(R.color.trans_white),
getContext().getResources().getColor(R.color.white)
);
mTopBar.setBackgroundColor(topBarColor);
mIvBack.getDrawable().mutate().setTint(iconColor);
mIvAssemble.getDrawable().mutate().setTint(iconColor);
}

/**

  • 根据upAlphaScalePro,设置logo、收藏icon缩放,搜索icon、分享icon透明度
    */
    private void alphaScaleByPro(float upAlphaScalePro) {
    float alpha = 1 - upAlphaScalePro;
    float scale = 1 - upAlphaScalePro;
    setAlpha(mIvSearch, alpha);
    setAlpha(mIvShare, alpha);
    setScaleAlpha(mIvLogo, scale, scale, alpha);
    setScaleAlpha(mVCollect, scale, scale, alpha);
    }

private float getDownContentAlphaPro() {
return (downEndY - MathUtils.clamp(mLlContent.getTranslationY(), downContentAlphaY, downEndY)) / (downEndY - downContentAlphaY);
}

private float getDownCollapsedAlphaPro() {
return (downCollapsedAlphaY - MathUtils.clamp(mLlContent.getTranslationY(), contentTransY, downCollapsedAlphaY)) / (downCollapsedAlphaY -contentTransY);
}

private float getDownShopBarTransPro() {
return (downShopBarTransY - MathUtils.clamp(mLlContent.getTranslationY(), contentTransY, downShopBarTransY)) / (downShopBarTransY -contentTransY);
}

private float getUpAlphaGradientPro() {
return (upAlphaScaleY - MathUtils.clamp(mLlContent.getTranslationY(), upAlphaGradientY, upAlphaScaleY)) / (upAlphaScaleY-upAlphaGradientY);
}

private float getUpAlphaScalePro() {
return (contentTransY - MathUtils.clamp(mLlContent.getTranslationY(), upAlphaScaleY, contentTransY)) / (contentTransY-upAlphaScaleY);
}

private float getUpCollapsedContentTransPro() {
return (contentTransY - MathUtils.clamp(mLlContent.getTranslationY(), topBarHeight, contentTransY)) / (contentTransY-topBarHeight);
}

private void setAlpha(View view, float alpha){
view.setAlpha(alpha);
}

private void setScale(View view ,float scaleY,float scaleX){
view.setScaleY(scaleY);
view.setScaleX(scaleX);
}
private void setScaleAlpha(View view ,float scaleY,float scaleX,float alpha){
setAlpha(view,alpha);
setScale(view,scaleY,scaleX);
}

private void translation(View view, float translationY) {
view.setTranslationY(translationY);
}

public interface ProgressUpdateListener{

void onUpCollapsedContentTransProUpdate(float pro);

void onUpAlphaScaleProUpdate(float pro);

void onUpAlphaGradientProUpdate(float pro);

void onDownCollapsedAlphaProUpdate(float pro);

void onDownContentAlphaProUpdate(float pro);

void onDownShopBarTransProUpdate(float pro);
}

onStopNestedScroll()

在下滑Content部分从初始状态转换到展开状态的过程中松手就会执行收起或展开的动画,这逻辑在onStopNestedScroll()实现,但注意如果动画未执行完毕手指再落下滑动时,应该在onNestedScrollAccepted()取消当前执行中的动画。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果Content部分的TransitionY没有超过downCollapsedAlphaY,执行收起Content部分动画效果,恢复到初始转态。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果Content部分的TransitionY超过了downCollapsedAlphaY,执行展开Content部分动画效果,转换到展开转态。 代码实现如下:


public static final long ANIM_DURATION_FRACTION = 200L;
private ValueAnimator restoreOrExpandAnimator;//收起或展开折叠内容时执行的动画

public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

restoreOrExpandAnimator = new ValueAnimator();
restoreOrExpandAnimator.setInterpolator(new AccelerateInterpolator());
restoreOrExpandAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
translation(mLlContent, (float) animation.getAnimatedValue());

//根据downShopBarTransPro,设置购物内容内容位移
float downShopBarTransPro = getDownShopBarTransPro();
transShopBarByPro(downShopBarTransPro);

//根据downCollapsedAlphaPro,设置折叠内容透明度
float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
alphaCollapsedContentByPro(downCollapsedAlphaPro);

//根据downContentAlphaPro,设置滑动内容、收起按钮的透明度
float downContentAlphaPro = getDownContentAlphaPro();
alphaContentByPro(downContentAlphaPro);

if (mProgressUpdateListener!=null){
mProgressUpdateListener.onDownCollapsedAlphaProUpdate(downCollapsedAlphaPro);
mProgressUpdateListener.onDownContentAlphaProUpdate(downContentAlphaPro);
mProgressUpdateListener.onDownShopBarTransProUpdate(downShopBarTransPro);
}
}
});
}

@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
mParentHelper.onNestedScrollAccepted(child, target, axes, type);
if (restoreOrExpandAnimator.isStarted()) {
restoreOrExpandAnimator.cancel();
}
}

@Override
public void onStopNestedScroll(@NonNull View target, int type) {
mParentHelper.onStopNestedScroll(target, type);
//如果不是从初始状态转换到展开状态过程触发返回
if (mLlContent.getTranslationY() <= contentTransY) {
return;
}

//根据百分比计算动画执行的时长
float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
float downContentAlphaYPro = getDownContentAlphaPro();
if (downCollapsedAlphaPro == 0) {
expand((long) (downContentAlphaYPro * ANIM_DURATION_FRACTION));
} else {
restore((long) (downCollapsedAlphaPro * ANIM_DURATION_FRACTION));
}
}

public void restore(long dur){
if (restoreOrExpandAnimator.isStarted()) {
restoreOrExpandAnimator.cancel();
}
restoreOrExpandAnimator.setFloatValues(mLlContent.getTranslationY(), contentTransY);
restoreOrExpandAnimator.setDuration(dur);
restoreOrExpandAnimator.start();
}

public void expand(long dur){
if (restoreOrExpandAnimator.isStarted()) {
restoreOrExpandAnimator.cancel();
}
restoreOrExpandAnimator.setFloatValues(mLlContent.getTranslationY(), downEndY);
restoreOrExpandAnimator.setDuration(dur);
restoreOrExpandAnimator.start();
}

处理惯性滑动

NestedScrollingParent2处理惯性滑动的方式主要有两种: 一、在onNestedPreFling()或者onNestedFling()返回值为true消费掉。 二、在onNestedPreFling()和onNestedFling()返回值都为false的前提下,在onNestedPreScroll()或者onNestedScroll()消费掉,这种方式可以和普通的滑动共用逻辑代码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

场景1:快速往上滑动Content部分的可滑动View产生惯性滑动,这和前面onNestedPreScroll()处理上滑的效果一模一样,因此可以复用逻辑,使用第二种方式处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

场景2:在折叠状态并Content部分的可滑动View没有滑动到顶部尽头时,快速往下滑动Content部分的可滑动View产生惯性滑动滑到顶部尽头就停止了,这和前面onNestedPreScroll()处理下滑的效果类似,但多了个惯性滑动滑到顶部尽头就停止的条件判断,使用第二种方式处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

场景3:快速往下滑动Content部分的可滑动View转化展开状态产生惯性滑动,这和前面onNestedPreScroll()处理下滑的效果类似,使用第二种方式处理,但注意在惯性滑动没有完全消费掉的时候,会不断触发onNestedPreScroll()来消费直到惯性滑动完全消费掉,所以当滑动到展开状态的时候要停止Content部分的View滑动因为这时已经是展开状态了,不需View继续滑动触发onNestedPreScroll(),注意NestedScrollView并没有暴露对外停止滑动的方法,只能反射获取它的OverScroller停止滑动。 下面是代码实现:

@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
float contentTransY = mLlContent.getTranslationY() - dy;
//处理上滑和场景1
if (dy > 0) {
if (contentTransY >= topBarHeight) {
translationByConsume(mLlContent, contentTransY, consumed, dy);
} else {
translationByConsume(mLlContent, topBarHeight, consumed, (mLlContent.getTranslationY() - topBarHeight));
}
}

if (dy < 0 && !target.canScrollVertically(-1)) {
//下滑时处理Fling,完全折叠时,下滑Recycler(或NestedScrollView) Fling滚动到列表顶部(或视图顶部)停止Fling,对应场景2
if (type == ViewCompat.TYPE_NON_TOUCH&&mLlContent.getTranslationY() == topBarHeight) {
stopViewScroll(target);
return;
}

//处理下滑
if (contentTransY >= topBarHeight && contentTransY <= downEndY) {
translationByConsume(mLlContent, contentTransY, consumed, dy);
} else {
translationByConsume(mLlContent, downEndY, consumed, downEndY - mLlContent.getTranslationY());
//对应场景3
if (target instanceof NestedScrollView) {
stopViewScroll(target);
}
}
}

}

/**

  • 停止View的滑动
    */
    private void stopViewScroll(View target){
    if (target instanceof RecyclerView) {
    ((RecyclerView) target).stopScroll();
    }
    if (target instanceof NestedScrollView) {
    try {
    Class<? extends NestedScrollView> clazz = ((NestedScrollView) target).getClass();
    Field mScroller = clazz.getDeclaredField(“mScroller”);
    mScroller.setAccessible(true);
    OverScroller overScroller = (OverScroller) mScroller.get(target);
    overScroller.abortAnimation();
    } catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
    }
    }
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

场景4:快速往下滑动Content部分的可滑动View,从非折叠状态转化展开状态产生惯性滑动,因为有回弹效果,所以逻辑处理和onNestedPreScroll()不一样,使用第一种方式处理。


private ValueAnimator reboundedAnim;//回弹动画

public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

reboundedAnim = new ValueAnimator();
reboundedAnim.setInterpolator(new DecelerateInterpolator());
reboundedAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
translation(mLlContent, (float) animation.getAnimatedValue());

//根据upAlphaScalePro,设置logo、收藏icon缩放,搜索icon、分享icon透明度
float upAlphaScalePro = getUpAlphaScalePro();
alphaScaleByPro(upAlphaScalePro);

//根据upAlphaGradientPro,设置topBar背景、返回icon、拼团icon颜色渐变值,搜索框透明度
float upAlphaGradientPro = getUpAlphaGradientPro();
alphaGradientByPro(upAlphaGradientPro);

//根据downCollapsedAlphaPro,设置折叠内容透明度
float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
alphaCollapsedContentByPro(downCollapsedAlphaPro);

//根据downShopBarTransPro,设置购物内容内容位移
float downShopBarTransPro = getDownShopBarTransPro();
transShopBarByPro(downShopBarTransPro);

//根据upCollapsedContentTransPro,设置折叠内容位移
float upCollapsedContentTransPro = getUpCollapsedContentTransPro();
transCollapsedContentByPro(upCollapsedContentTransPro);

if (mProgressUpdateListener!=null){
mProgressUpdateListener.onUpAlphaScaleProUpdate(upAlphaScalePro);
mProgressUpdateListener.onUpAlphaGradientProUpdate(upAlphaGradientPro);
mProgressUpdateListener.onDownCollapsedAlphaProUpdate(downCollapsedAlphaPro);
mProgressUpdateListener.onDownShopBarTransProUpdate(downShopBarTransPro);
mProgressUpdateListener.onUpCollapsedContentTransProUpdate(upCollapsedContentTransPro);
}
}
});
reboundedAnim.setDuration(ANIM_DURATION_FRACTION);
}

@Override
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
return false;
}

@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
if (velocityY<0){//往下滑动的惯性滑动
float translationY = mLlContent.getTranslationY();
float dy = translationY - velocityY;
if (translationY >topBarHeight && translationY<= downFlingCutOffY) {//非折叠状态
//根据dy设置动画结束值,只有dy>contentTransY才会有回弹,downFlingCutOffY是回弹的最大值
if (dy<=contentTransY){
reboundedAnim.setFloatValues(translationY,dy);
}else if (dy>contentTransY&&dy<downFlingCutOffY){
reboundedAnim.setFloatValues(translationY,dy,contentTransY);
}else {
reboundedAnim.setFloatValues(translationY,downFlingCutOffY,contentTransY);
}
reboundedAnim.start();
return true;
}
}
return false;
}

释放资源

在View从Window上移除时候,执行要停止动画、释放监听者的操作。

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (restoreOrExpandAnimator.isStarted()) {
restoreOrExpandAnimator.cancel();
restoreOrExpandAnimator.removeAllUpdateListeners();
restoreOrExpandAnimator = null;
}

if(reboundedAnim.isStarted()){
reboundedAnim.cancel();
reboundedAnim.removeAllUpdateListeners();
reboundedAnim = null;
}

if (mProgressUpdateListener!=null){
mProgressUpdateListener=null;
}
}

商家点餐页的嵌套滑动实现

效果分析

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

商家点餐页布局主要有上图五部分组成,逻辑上的层级如图所示有3层。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

滑动Content部分时利用View的TransitionY属性改变位置来消耗滑动值,根据Content部分的TransitionY设定各种范围从而计算百分比来执行位移、Alpha效果。下面来说明上图中变量的意义:

contentTransY;//滑动内容初始化TransY
iconTransY;//分享、关闭icon初始化transY
upEndIconTransY;//分享、关闭icon上滑最终transY
downFlingCutOffY;///从展开状态下滑产生fling时回弹到初始状态的最大值

ElemeFoodNestedScrollLayout

商家点餐页的嵌套滑的自定义View继承FrameLayout、实现NestedScrollingParent2接口,命名为ElemeFoodNestedScrollLayout。

布局

下面是布局要点,侧重于控件的尺寸和位置,完整布局请参考:ElemeFoodNestedScrollLayout布局

<android.support.v4.widget.NestedScrollView
android:id=“@+id/ns”
android:translationY=“@dimen/food_content_trans_y”
android:fillViewport=“true”
android:background=“@android:color/white”
android:layout_width=“match_parent”
android:layout_height=“match_parent”


<android.support.constraint.ConstraintLayout
android:layout_width=“match_parent”
android:layout_gravity=“bottom”
android:layout_height=“@dimen/shop_bar_height”
android:translationY=“@dimen/shop_bar_height”

绑定需要做效果的View、引入Dimens、设置Content部分的初始化TransitionY

从上面图片能够分析出:关闭状态时,Content部分的TransY为满屏高度

public class ElemeFoodNestedScrollLayout extends FrameLayout implements NestedScrollingParent2 {

//shopBar部分
private View mShopBar;

//content部分
private View mNestedScrollView;
private View mTvComm;
private View mTvGoodCommRate;
private View mTvCommDetail;
private View mTvCommCount;
private View mVLine;
private View mTvFoodDetail;

//expand部分
private View mIvExpand;

//icon部分
private View mIvShare;
private View mIvClose;

//mask部分
private View mVMask;

private float shopBarHeight;//shopBar部分高度
private float ivExpandHegiht;//ivExpand部分高度
private float statusBarHeight;//状态栏高度
private float iconTransY;//分享、关闭icon初始化transY
private float contentTransY;//滑动内容初始化TransY
private float downFlingCutOffY;//下滑时fling上部分回弹临界值
private float upEndIconTransY;//分享、关闭icon上滑最终transY

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
shopBarHeight = getResources().getDimension(R.dimen.shop_bar_height);
ivExpandHegiht = getResources().getDimension(R.dimen.iv_food_expand);
contentTransY = getResources().getDimension(R.dimen.food_content_trans_y);
iconTransY = getResources().getDimension(R.dimen.iv_food_icon_trans_y);
statusBarHeight = getResources().getDimensionPixelSize(getResources().getIdentifier(“status_bar_height”, “dimen”, “android”));
downFlingCutOffY = contentTransY + dp2px(92);
upEndIconTransY = statusBarHeight + dp2px(8);
//因为开始就是关闭状态,设置Content部分的TransY为满屏高度
mNestedScrollView.setTranslationY(getMeasuredHeight());
}

@Override
protected void onFinishInflate() {
super.onFinishInflate();
mNestedScrollView = findViewById(R.id.ns);
mShopBar = findViewById(R.id.cl_food_shop_bar);

mTvComm = findViewById(R.id.t_comm);
mTvGoodCommRate = findViewById(R.id.t_good_comm_rate);
mTvCommDetail = findViewById(R.id.t_comm_detail);
mTvFoodDetail = findViewById(R.id.t_food_detail);

mTvCommCount = findViewById(R.id.t_comm_count);
mVLine = findViewById(R.id.view_line);

mIvExpand = findViewById(R.id.iv_food_expand);
mIvShare = findViewById(R.id.iv_small_share);
mIvClose = findViewById(R.id.iv_small_close);
mVMask = findViewById(R.id.v_mask);
}
}

实现NestedScrollingParent2接口

onStartNestedScroll()

ElemeFoodNestedScrollLayout只处理Content部分里可滑动View的垂直方向的滑动。

@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
//处理Content部分里可滑动View的垂直方向的滑动
return axes == ViewCompat.SCROLL_AXIS_VERTICAL && target.getId() == R.id.ns;
}

onNestedPreScroll()

接下来就是处理滑动,上面效果分析提过:Content部分的上滑范围=[0,contentTransY]、 下滑范围=[contentTransY,即满屏高度]即滑动范围为[0,即满屏高度],ElemeFoodNestedScrollLayout要控制Content部分的TransitionY值要在范围内,之前的商家页已经说过了具体思路,这里不再赘述。


private ProgressUpdateListener mProgressUpdateListener;

@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
float contentTransY = target.getTranslationY() - dy;
//处理上滑
if (dy > 0) {
if (contentTransY >= 0) {
translationByConsume(target, contentTransY, consumed, dy);
} else {
translationByConsume(target, 0, consumed, (target.getTranslationY() - 0));
}
}

//处理下滑
if (dy < 0 && !target.canScrollVertically(-1)) {
if (contentTransY >= 0 && contentTransY < getMeasuredHeight()) {
translationByConsume(target, contentTransY, consumed, dy);
} else {
translationByConsume(target, getMeasuredHeight(), consumed, getMeasuredHeight() - target.getTranslationY());
}
}

alphaTransView(contentTransY);

if (mProgressUpdateListener!=null){
mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
}
}

private void alphaTransView(float transY) {
float upCollapseTransPro = getUpExpandTransPro();
//位移购物内容
float shopBarTransY = (1 - upCollapseTransPro) * shopBarHeight;
translation(mShopBar, shopBarTransY);

//设置商品信息View的透明度变化
setAlpha(mTvComm, upCollapseTransPro);
setAlpha(mTvGoodCommRate, upCollapseTransPro);
setAlpha(mTvCommDetail, upCollapseTransPro);
setAlpha(mTvFoodDetail, upCollapseTransPro);
setAlpha(mTvCommCount, 1 - upCollapseTransPro);
setAlpha(mVLine, 1 - upCollapseTransPro);

//位移share close两个Icon,设置展开icon透明度
if (transY <= contentTransY) {
float ivExpandUpTransY = upCollapseTransPro * -contentTransY;
translation(mIvExpand, ivExpandUpTransY);
setAlpha(mIvExpand, 1 - upCollapseTransPro);

float iconTransY = upEndIconTransY + (1 - upCollapseTransPro) * (this.iconTransY - upEndIconTransY);
translation(mIvShare, iconTransY);
translation(mIvClose, iconTransY);

} else if (transY > contentTransY && transY <= getMeasuredHeight()) {
float ivExpandDowndTransY = (1 - getDownIvExpnadPro()) * ivExpandHegiht;
translation(mIvExpand, ivExpandDowndTransY);

float iconTransY = transY + dp2px(16);
translation(mIvShare, iconTransY);
translation(mIvClose, iconTransY);
}
}

private float getDownConetntClosePro() {
return (mNestedScrollView.getTranslationY() - contentTransY) / (getMeasuredHeight() - contentTransY);
}

private float getDownIvExpnadPro() {
return ((contentTransY+ivExpandHegiht)-MathUtils.clamp(mNestedScrollView.getTranslationY(), contentTransY, contentTransY+ivExpandHegiht)) / ivExpandHegiht;
}

private float getUpExpandTransPro() {
return (contentTransY - MathUtils.clamp(mNestedScrollView.getTranslationY(), 0, contentTransY)) / contentTransY;
}

public interface ProgressUpdateListener{
void onDownConetntCloseProUpdate(float pro);
}

onStopNestedScroll()

在从初始状态到展开状态的上滑过程中松手,若上滑百分比小于等于50%则执行恢复到初始状态的动画,否则执行转化到展开状态的动画;同理从初始状态到关闭状态下滑过程中松手,若下滑百分比小于等于50%则执行恢复到初始状态的动画,否则执行转化到关闭状态的动画;


private ValueAnimator restoreOrExpandOrCloseAnimator;//收起或展开折叠内容时执行的动画
private NestedScrollingParentHelper mParentHelper;

public ElemeFoodNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mParentHelper = new NestedScrollingParentHelper(this);

restoreOrExpandOrCloseAnimator = new ValueAnimator();
restoreOrExpandOrCloseAnimator.setInterpolator(new AccelerateInterpolator());
restoreOrExpandOrCloseAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
translation(mNestedScrollView, (float) animation.getAnimatedValue());
alphaTransView(mNestedScrollView.getTranslationY());
if (mProgressUpdateListener!=null){
mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
}
}
});
restoreOrExpandOrCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
int alpha=mNestedScrollView.getTranslationY() >= getMeasuredHeight()?0:1;
setAlpha(mIvClose,alpha);
setAlpha(mIvShare,alpha);
}
});
}

@Override
public void onStopNestedScroll(@NonNull View target, int type) {
mParentHelper.onStopNestedScroll(target, type);
float translationY = target.getTranslationY();
if (translationY == contentTransY|| reboundedAnim.isStarted()|| restoreOrExpandOrCloseAnimator.isStarted()) {
return;
}

long dur;
if (translationY < contentTransY) {
if (getUpExpandTransPro() <= 0.5f) {
dur = (long) (getUpExpandTransPro() * ANIM_DURATION_FRACTION);
restore(dur);
} else {
dur = (long) ((1 - getUpExpandTransPro()) * ANIM_DURATION_FRACTION);
expand(dur);
}
} else {
if (getDownConetntClosePro() >= 0.5f) {
dur = (long) (getDownConetntClosePro() * ANIM_DURATION_FRACTION);
close(dur);
} else {
dur = (long) ((1 - getDownConetntClosePro()) * ANIM_DURATION_FRACTION);
restore(dur);
}
}
}

public void restore(long dur) {
mIvClose.setClickable(true);
mVMask.setClickable(true);
mIvExpand.setClickable(true);
if (restoreOrExpandOrCloseAnimator.isStarted()) {
restoreOrExpandOrCloseAnimator.cancel();
}
restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), contentTransY);
restoreOrExpandOrCloseAnimator.setDuration(dur);
restoreOrExpandOrCloseAnimator.start();
}

public void expand(long dur) {
if (restoreOrExpandOrCloseAnimator.isStarted()) {
restoreOrExpandOrCloseAnimator.cancel();
}
restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), 0);
restoreOrExpandOrCloseAnimator.setDuration(dur);
restoreOrExpandOrCloseAnimator.start();
}

public void close(long dur) {
mNestedScrollView.scrollTo(0,0);
mIvClose.setClickable(false);
mVMask.setClickable(false);
mIvExpand.setClickable(false);
if (restoreOrExpandOrCloseAnimator.isStarted()) {
restoreOrExpandOrCloseAnimator.cancel();
}
restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), getMeasuredHeight());
restoreOrExpandOrCloseAnimator.setDuration(dur);
restoreOrExpandOrCloseAnimator.start();
}

处理惯性滑动

这里有两个场景需要使用onNestedPreFling()处理惯性滑动: 1、从展开状态下滑时处理回弹Fling,执行回弹动画; 2、从初始状态到关闭状态下滑百分比超过50%惯性滑动关闭;

private ValueAnimator reboundedAnim;//回弹动画

public ElemeFoodNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

reboundedAnim = new ValueAnimator();
reboundedAnim.setInterpolator(new DecelerateInterpolator());
reboundedAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
translation(mNestedScrollView, (float) animation.getAnimatedValue());
alphaTransView(mNestedScrollView.getTranslationY());
if (mProgressUpdateListener!=null){
mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
}
}
});
reboundedAnim.setDuration(ANIM_DURATION_FRACTION);
}

@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
if (velocityY<0) {
float translationY = target.getTranslationY();
float dy = translationY - velocityY;
//从展开状态下滑时处理回弹Fling,执行回弹动画
if (translationY >= 0 && translationY <= downFlingCutOffY){
if (dy<contentTransY){
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

如果你进阶的路上缺乏方向,可以加入我们的圈子和安卓开发者们一起学习交流!

  • Android进阶学习全套手册

    img

  • Android对标阿里P7学习视频

    img

  • BATJ大厂Android高频面试题

    img

最后,借用我最喜欢的乔布斯语录,作为本文的结尾:

人这一辈子没法做太多的事情,所以每一件都要做得精彩绝伦。
你的时间有限,所以不要为别人而活。不要被教条所限,不要活在别人的观念里。不要让别人的意见左右自己内心的声音。
最重要的是,勇敢的去追随自己的心灵和直觉,只有自己的心灵和直觉才知道你自己的真实想法,其他一切都是次要。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

如果你进阶的路上缺乏方向,可以加入我们的圈子和安卓开发者们一起学习交流!

  • Android进阶学习全套手册

    [外链图片转存中…(img-izKbeYEw-1712014723776)]

  • Android对标阿里P7学习视频

    [外链图片转存中…(img-cvHv5I7a-1712014723776)]

  • BATJ大厂Android高频面试题

    [外链图片转存中…(img-L5XlEHna-1712014723776)]

最后,借用我最喜欢的乔布斯语录,作为本文的结尾:

人这一辈子没法做太多的事情,所以每一件都要做得精彩绝伦。
你的时间有限,所以不要为别人而活。不要被教条所限,不要活在别人的观念里。不要让别人的意见左右自己内心的声音。
最重要的是,勇敢的去追随自己的心灵和直觉,只有自己的心灵和直觉才知道你自己的真实想法,其他一切都是次要。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值