自定义 Behavior - 仿新浪微博发现页的实现

}

public WeiboHeaderPagerBehavior(Context context, AttributeSet attrs) {

super(context, attrs);

init();

}

private void init() {

mOverScroller = new OverScroller(BaseAPP.getAppContext());

}

@Override

protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {

super.layoutChild(parent, child, layoutDirection);

mParent = new WeakReference(parent);

mChild = new WeakReference(child);

}

@Override

public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View

directTargetChild, View target, int nestedScrollAxes) {

if (BuildConfig.DEBUG) {

Log.d(TAG, “onStartNestedScroll: nestedScrollAxes=” + nestedScrollAxes);

}

boolean canScroll = canScroll(child, 0);

//拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩

return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&

!isClosed(child);

}

@Override

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,

float velocityX, float velocityY) {

// consumed the flinging behavior until Closed

boolean coumsed = !isClosed(child);

Log.i(TAG, “onNestedPreFling: coumsed=” +coumsed);

return coumsed;

}

@Override

public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,

float velocityX, float velocityY, boolean consumed) {

Log.i(TAG, “onNestedFling: velocityY=” +velocityY);

return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY,

consumed);

}

private boolean isClosed(View child) {

boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();

return isClosed;

}

public boolean isClosed() {

return mCurState == STATE_CLOSED;

}

private void changeState(int newState) {

if (mCurState != newState) {

mCurState = newState;

if (mCurState == STATE_OPENED) {

if (mPagerStateListener != null) {

mPagerStateListener.onPagerOpened();

}

} else {

if (mPagerStateListener != null) {

mPagerStateListener.onPagerClosed();

}

}

}

}

// 表示 Header TransLationY 的值是否达到我们指定的阀值, headerOffsetRange,到达了,返回 false,

// 否则,返回 true。注意 TransLationY 是负数。

private boolean canScroll(View child, float pendingDy) {

int pendingTranslationY = (int) (child.getTranslationY() - pendingDy);

int headerOffsetRange = getHeaderOffsetRange();

if (pendingTranslationY >= headerOffsetRange && pendingTranslationY <= 0) {

return true;

}

return false;

}

@Override

public boolean onInterceptTouchEvent(CoordinatorLayout parent, final View child, MotionEvent

ev) {

boolean closed = isClosed();

Log.i(TAG, “onInterceptTouchEvent: closed=” + closed);

if (ev.getAction() == MotionEvent.ACTION_UP && !closed) {

handleActionUp(parent,child);

}

return super.onInterceptTouchEvent(parent, child, ev);

}

@Override

public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,

int dx, int dy, int[] consumed) {

super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);

//dy>0 scroll up;dy<0,scroll down

Log.i(TAG, “onNestedPreScroll: dy=” + dy);

float halfOfDis = dy;

// 不能滑动了,直接给 Header 设置 终值,防止出错

if (!canScroll(child, halfOfDis)) {

child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);

} else {

child.setTranslationY(child.getTranslationY() - halfOfDis);

}

//consumed all scroll behavior after we started Nested Scrolling

consumed[1] = dy;

}

// 需要注意的是 Header 我们是通过 setTranslationY 来移出屏幕的,所以这个值是负数

private int getHeaderOffsetRange() {

return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen

.weibo_header_offset);

}

private void handleActionUp(CoordinatorLayout parent, final View child) {

if (BuildConfig.DEBUG) {

Log.d(TAG, "handleActionUp: ");

}

if (mFlingRunnable != null) {

child.removeCallbacks(mFlingRunnable);

mFlingRunnable = null;

}

mFlingRunnable = new FlingRunnable(parent, child);

if (child.getTranslationY() < getHeaderOffsetRange() / 6.0f) {

mFlingRunnable.scrollToClosed(DURATION_SHORT);

} else {

mFlingRunnable.scrollToOpen(DURATION_SHORT);

}

}

private void onFlingFinished(CoordinatorLayout coordinatorLayout, View layout) {

changeState(isClosed(layout) ? STATE_CLOSED : STATE_OPENED);

}

public void openPager() {

openPager(DURATION_LONG);

}

/**

  • @param duration open animation duration

*/

public void openPager(int duration) {

View child = mChild.get();

CoordinatorLayout parent = mParent.get();

if (isClosed() && child != null) {

if (mFlingRunnable != null) {

child.removeCallbacks(mFlingRunnable);

mFlingRunnable = null;

}

mFlingRunnable = new FlingRunnable(parent, child);

mFlingRunnable.scrollToOpen(duration);

}

}

public void closePager() {

closePager(DURATION_LONG);

}

/**

  • @param duration close animation duration

*/

public void closePager(int duration) {

View child = mChild.get();

CoordinatorLayout parent = mParent.get();

if (!isClosed()) {

if (mFlingRunnable != null) {

child.removeCallbacks(mFlingRunnable);

mFlingRunnable = null;

}

mFlingRunnable = new FlingRunnable(parent, child);

mFlingRunnable.scrollToClosed(duration);

}

}

private FlingRunnable mFlingRunnable;

/**

  • For animation , Why not use {@link android.view.ViewPropertyAnimator } to play animation

  • is of the

  • other {@link CoordinatorLayout.Behavior} that depend on this could not receiving the

  • correct result of

  • {@link View#getTranslationY()} after animation finished for whatever reason that i don’t know

*/

private class FlingRunnable implements Runnable {

private final CoordinatorLayout mParent;

private final View mLayout;

FlingRunnable(CoordinatorLayout parent, View layout) {

mParent = parent;

mLayout = layout;

}

public void scrollToClosed(int duration) {

float curTranslationY = ViewCompat.getTranslationY(mLayout);

float dy = getHeaderOffsetRange() - curTranslationY;

if (BuildConfig.DEBUG) {

Log.d(TAG, “scrollToClosed:offest:” + getHeaderOffsetRange());

Log.d(TAG, “scrollToClosed: cur0:” + curTranslationY + “,end0:” + dy);

Log.d(TAG, “scrollToClosed: cur:” + Math.round(curTranslationY) + “,end:” + Math

.round(dy));

Log.d(TAG, “scrollToClosed: cur1:” + (int) (curTranslationY) + “,end:” + (int) dy);

}

mOverScroller.startScroll(0, Math.round(curTranslationY - 0.1f), 0, Math.round(dy +

0.1f), duration);

start();

}

public void scrollToOpen(int duration) {

float curTranslationY = ViewCompat.getTranslationY(mLayout);

mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY,

duration);

start();

}

private void start() {

if (mOverScroller.computeScrollOffset()) {

mFlingRunnable = new FlingRunnable(mParent, mLayout);

ViewCompat.postOnAnimation(mLayout, mFlingRunnable);

} else {

onFlingFinished(mParent, mLayout);

}

}

@Override

public void run() {

if (mLayout != null && mOverScroller != null) {

if (mOverScroller.computeScrollOffset()) {

if (BuildConfig.DEBUG) {

Log.d(TAG, "run: " + mOverScroller.getCurrY());

}

ViewCompat.setTranslationY(mLayout, mOverScroller.getCurrY());

ViewCompat.postOnAnimation(mLayout, this);

} else {

onFlingFinished(mParent, mLayout);

}

}

}

}

/**

  • callback for HeaderPager 's state

*/

public interface OnPagerStateListener {

/**

  • do callback when pager closed

*/

void onPagerClosed();

/**

  • do callback when pager opened

*/

void onPagerOpened();

}

}

第二个关键点的实现

在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。

在第一个关键点的实现上,我们是通过自定义 Behavior 来处理 ViewPager 里面 RecyclerView 的移动的,那我们要怎样监听整个 Header 的滑动了。

那就是重写 LinearLayout,将滑动事件交给 ScrollingParent(这里是CoordinatorLayout) 去处理,CoordinatorLayout 再交给子 View 的 behavior 去处理。

public class NestedLinearLayout extends LinearLayout implements NestedScrollingChild {

private static final String TAG = “NestedLinearLayout”;

private final int[] offset = new int[2];

private final int[] consumed = new int[2];

private NestedScrollingChildHelper mScrollingChildHelper;

private int lastY;

public NestedLinearLayout(Context context) {

this(context, null);

}

public NestedLinearLayout(Context context, @Nullable AttributeSet attrs) {

this(context, attrs, 0);

}

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

super(context, attrs, defStyleAttr);

initData();

}

private void initData() {

if (mScrollingChildHelper == null) {

mScrollingChildHelper = new NestedScrollingChildHelper(this);

mScrollingChildHelper.setNestedScrollingEnabled(true);

}

}

@Override

public boolean onInterceptTouchEvent(MotionEvent event) {

switch (event.getAction()){

case MotionEvent.ACTION_DOWN:

lastY = (int) event.getRawY();

// 当开始滑动的时候,告诉父view

startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL

| ViewCompat.SCROLL_AXIS_VERTICAL);

break;

case MotionEvent.ACTION_MOVE:

return true;

}

return super.onInterceptTouchEvent(event);

}

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()){

case MotionEvent.ACTION_MOVE:

Log.i(TAG, “onTouchEvent: ACTION_MOVE=”);

int y = (int) (event.getRawY());

int dy =lastY- y;

lastY = y;

Log.i(TAG, “onTouchEvent: lastY=” + lastY);

Log.i(TAG, “onTouchEvent: dy=” + dy);

// dy < 0 下拉, dy>0 赏花

if (dy >0) { // 上滑的时候才交给父类去处理

if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类

&& dispatchNestedPreScroll(0, dy, consumed, offset)) {//

// 父类进行了一部分滚动

}

}else{

if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类

&& dispatchNestedScroll(0, 0, 0,dy, offset)) {//

// 父类进行了一部分滚动

}

}

break;

}

return true;

}

private NestedScrollingChildHelper getScrollingChildHelper() {

return mScrollingChildHelper;

}

// 接口实现--------------------------------------------------

@Override

public void setNestedScrollingEnabled(boolean enabled) {

getScrollingChildHelper().setNestedScrollingEnabled(enabled);

}

@Override

public boolean isNestedScrollingEnabled() {

return getScrollingChildHelper().isNestedScrollingEnabled();

}

@Override

public boolean startNestedScroll(int axes) {

return getScrollingChildHelper().startNestedScroll(axes);

}

@Override

public void stopNestedScroll() {

getScrollingChildHelper().stopNestedScroll();

}

@Override

public boolean hasNestedScrollingParent() {

return getScrollingChildHelper().hasNestedScrollingParent();

}

@Override

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,

int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {

return getScrollingChildHelper().dispatchNestedScroll(dxConsumed,

dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);

}

@Override

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed,

int[] offsetInWindow) {

return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy,

consumed, offsetInWindow);

}

@Override

public boolean dispatchNestedFling(float velocityX, float velocityY,

boolean consumed) {

return getScrollingChildHelper().dispatchNestedFling(velocityX,

velocityY, consumed);

}

@Override

public boolean dispatchNestedPreFling(float velocityX, float velocityY) {

return getScrollingChildHelper().dispatchNestedPreFling(velocityX,

velocityY);

}

}


Content 部分的实现


Content 部分的实现也主要有两个关键点

  • 整体置于 Header 之下

  • Content 跟着 Header 移动。即 Header 位置发生变化的时候,Content 也需要随着调整位置。

第一个关键点的实现

整体置于 Header 之下。这个我们可以参考 APPBarLayout 的 behavior,它是这样处理的。

/**

  • Copy from Android design library

  • Created by xujun

*/

public abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior {

private final Rect mTempRect1 = new Rect();

private final Rect mTempRect2 = new Rect();

private int mVerticalLayoutGap = 0;

private int mOverlayTop;

public HeaderScrollingViewBehavior() {

}

public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {

super(context, attrs);

}

@Override

public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {

final int childLpHeight = child.getLayoutParams().height;

if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {

// If the menu’s height is set to match_parent/wrap_content then measure it

// with the maximum visible height

final List dependencies = parent.getDependencies(child);

final View header = findFirstDependency(dependencies);

if (header != null) {

if (ViewCompat.getFitsSystemWindows(header) && !ViewCompat.getFitsSystemWindows(child)) {

// If the header is fitting system windows then we need to also,

// otherwise we’ll get CoL’s compatible measuring

ViewCompat.setFitsSystemWindows(child, true);

if (ViewCompat.getFitsSystemWindows(child)) {

// If the set succeeded, trigger a new layout and return true

child.requestLayout();

return true;

}

}

if (ViewCompat.isLaidOut(header)) {

int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);

if (availableHeight == 0) {

// If the measure spec doesn’t specify a size, use the current height

availableHeight = parent.getHeight();

}

final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);

final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,

childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT ? View.MeasureSpec.EXACTLY : View.MeasureSpec.AT_MOST);

// Now measure the scrolling view with the correct height

parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);

return true;

}

}

}

return false;

}

@Override

protected void layoutChild(final CoordinatorLayout parent, final View child, final int layoutDirection) {

final List dependencies = parent.getDependencies(child);

final View header = findFirstDependency(dependencies);

if (header != null) {

final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();

final Rect available = mTempRect1;

available.set(parent.getPaddingLeft() + lp.leftMargin, header.getBottom() + lp.topMargin,

parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,

parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);

final Rect out = mTempRect2;

GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), available, out, layoutDirection);

final int overlap = getOverlapPixelsForOffset(header);

child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);

mVerticalLayoutGap = out.top - header.getBottom();

} else {

// If we don’t have a dependency, let super handle it

super.layoutChild(parent, child, layoutDirection);

mVerticalLayoutGap = 0;

}

}

float getOverlapRatioForOffset(final View header) {

return 1f;

}

final int getOverlapPixelsForOffset(final View header) {

return mOverlayTop == 0

? 0
MathUtils.constrain(Math.round(getOverlapRatioForOffset(header) * mOverlayTop),

0, mOverlayTop);

}

private static int resolveGravity(int gravity) {

return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;

}

protected abstract View findFirstDependency(List views);

protected int getScrollRange(View v) {

return v.getMeasuredHeight();

}

/**

  • The gap between the top of the scrolling view and the bottom of the header layout in pixels.

*/

final int getVerticalLayoutGap() {

return mVerticalLayoutGap;

}

/**

  • Set the distance that this view should overlap any {@link AppBarLayout}.

  • @param overlayTop the distance in px

*/

public final void setOverlayTop(int overlayTop) {

mOverlayTop = overlayTop;

}

/**

  • Returns the distance that this view should overlap any {@link AppBarLayout}.

*/

public final int getOverlayTop() {

return mOverlayTop;

}

}

这个基类的代码还是很好理解的,因为之前就说过了,正常来说被依赖的 View 会优先于依赖它的 View 处理,所以需要依赖的 View 可以在 measure/layout 的时候,找到依赖的 View 并获取到它的测量/布局的信息,这里的处理就是依靠着这种关系来实现的.

我们的实现类,需要重写的除了抽象方法 findFirstDependency 外,还需要重写 getScrollRange,我们把 Header

的 Id id_weibo_header 定义在 ids.xml 资源文件内,方便依赖的判断.

至于缩放的高度,根据 结果图 得知是 0,得出如下代码

private int getFinalHeight() {

Resources resources = BaseAPP.getInstance().getResources();

return 0;

}

@Override

protected int getScrollRange(View v) {

if (isDependOn(v)) {

return Math.max(0, v.getMeasuredHeight() - getFinalHeight());

} else {

return super.getScrollRange(v);

}

}

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
外,还需要重写 getScrollRange,我们把 Header

的 Id id_weibo_header 定义在 ids.xml 资源文件内,方便依赖的判断.

至于缩放的高度,根据 结果图 得知是 0,得出如下代码

private int getFinalHeight() {

Resources resources = BaseAPP.getInstance().getResources();

return 0;

}

@Override

protected int getScrollRange(View v) {

if (isDependOn(v)) {

return Math.max(0, v.getMeasuredHeight() - getFinalHeight());

} else {

return super.getScrollRange(v);

}

}

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

[外链图片转存中…(img-v6KfwYqT-1715363335600)]

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!
[外链图片转存中…(img-PBkiXjfN-1715363335601)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值