Android 仿滴滴首页嵌套滑动效果

目录表

  • android 自定义view必备api
  • android 可拖动圆环刻度条
  • android 仿滴滴大头针跳动波纹效果
  • android 仿网易云鲸云音效
  • Android 仿滴滴首页嵌套滑动效果

    这是最终的实现效果,由于使用的模拟器录制,所以顶部地图的渲染效果不是很好。
    Android 仿滴滴首页嵌套滑动效果

    在说代码之前,可以先看下最终的 CompNsViewGroup XML 结构,CompNsViewGroup 内部包含顶部地图 MapView 和滑动布局 LinearLayout,而 LinearLayout 布局的内部即我们常用的滑动控件 RecyclerView,在这里为何还要加层 LinearLayout 呢?这样做的好处是,我们可以更好的适配不同滑动控件,而不仅仅是将CompNsViewGroup 与 RecyclerView 耦合住。

        <com.comp.ns.CompNsViewGroup
    android:id="@+id/dd_view_group"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    didi:header_id="@+id/t_map_view"
    didi:target_id="@+id/target_layout"
    didi:inn_id="@+id/inner_rv"
    didi:header_init_top="0"
    didi:target_init_bottom="250">

    <com.tencent.tencentmap.mapsdk.maps.MapView
    android:id="@+id/t_map_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    <LinearLayout
    android:id="@+id/target_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="#fff">

    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/inner_rv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

    </LinearLayout>

    </com.comp.ns.CompNsViewGroup>

    实现

    在 attrs.xml 文件下为 CompNsViewGroup 添加自定义属性,其中 header_id 对应顶部地图 MapView,target_id 对应滑动布局 LinearLayout,inn_id 对应滑动控件RecyclerView。

    <resources>
    <declare-styleable name="CompNsViewGroup">
    <attr name="header_id"/>
    <attr name="target_id"/>
    <attr name="inn_id"/>
    <attr name="header_init_top" format="integer"/>
    <attr name="target_init_bottom" format="integer"/>
    </declare-styleable>
    </resources>

    我们根据 attrs.xml 中的属性,获取 XML 中 CompNsViewGroup 中的 View ID

            // 获取配置参数
    final TypedArray array = context.getTheme().obtainStyledAttributes(attrs
    , R.styleable.CompNsViewGroup
    , defStyleAttr, 0);
    mHeaderResId = array.getResourceId
    (R.styleable.CompNsViewGroup_header_id, -1);
    mTargetResId = array.getResourceId
    (R.styleable.CompNsViewGroup_target_id, -1);
    mInnerScrollId = array.getResourceId
    (R.styleable.CompNsViewGroup_inn_id, -1);
    if (mHeaderResId == -1 || mTargetResId == -1
    || mInnerScrollId == -1)
    throw new RuntimeException("VIEW ID is null");

    我们根据 attrs.xml 中的属性,来初始化 View 的高度、距离等,计算高度时,需要考虑到状态栏因素

            mHeaderInitTop = Utils.dip2px(getContext()
    , array.getInt(R.styleable.CompNsViewGroup_header_init_top, 0));
    mHeaderCurrTop = mHeaderInitTop;
    // 屏幕高度 - 底部距离 - 状态栏高度
    mTargetInitBottom = Utils.dip2px(getContext()
    , array.getInt(R.styleable.CompNsViewGroup_target_init_bottom, 0));
    // 注意:当前activity默认去掉了标题栏
    mTargetInitTop = Utils.getScreenHeight(getContext()) - mTargetInitBottom
    - Utils.getStatusBarHeight(getContext().getApplicationContext());
    mTargetCurrTop = mTargetInitTop;

    通过上面获取到的 View ID,我们能够直接引用到 XML 中的相关 View 实例,而后续的滑动,本质上就是针对该 View 所进行的一系列判断处理。

        @Override
    protected void onFinishInflate() {
    super.onFinishInflate();
    mHeaderView = findViewById(mHeaderResId);
    mTargetView = findViewById(mTargetResId);
    mInnerScrollView = findViewById(mInnerScrollId);
    }

    我们重写 onMeasure 方法,其不仅是给 childView 传入测量值和测量模式,还将我们自己测量的尺寸提供给父 ViewGroup 让其给我们提供期望大小的区域。

        @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 计算子VIEW的尺寸
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    int widthModle = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightModle = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    switch (widthModle) {
    case MeasureSpec.AT_MOST:
    case MeasureSpec.UNSPECIFIED:
    // TODO:wrap_content 暂不考虑
    break;

    case MeasureSpec.EXACTLY:
    // 全屏或者固定尺寸
    break;
    }

    switch (heightModle) {
    case MeasureSpec.UNSPECIFIED:
    case MeasureSpec.AT_MOST:
    break;

    case MeasureSpec.EXACTLY:
    break;
    }

    setMeasuredDimension(widthSize, heightSize);
    }

    我们重写 onLayout 方法,给 childView 确定位置。需要注意的是,原始 bottom 不是 height 高度,而是又向下挪了 mTargetInitTop,我们可以想象成,我们一直将 mTargetView 挪动到了屏幕下方看不到的地方。

        @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int childCount = getChildCount();
    if (childCount == 0)
    return;
    final int width = getMeasuredWidth();
    final int height = getMeasuredHeight();

    // 注意:原始bottom不是height高度,而是又向下挪了mTargetInitTop
    mTargetView.layout(getPaddingLeft()
    , getPaddingTop() + mTargetCurrTop
    , width - getPaddingRight()
    , height + mTargetCurrTop
    + getPaddingTop() + getPaddingBottom());

    int headerWidth = mHeaderView.getMeasuredWidth();
    int headerHeight = mHeaderView.getMeasuredHeight();
    mHeaderView.layout((width - headerWidth)/2
    , mHeaderCurrTop + getPaddingTop()
    , (width + headerWidth)/2
    , headerHeight + mHeaderCurrTop + getPaddingTop());
    }

    此功能实现的核心即事件的分发和拦截了。在接收到事件时,如果上次滚动还未结束,则先停下。随后判断TargetView 内的 RecyclerView 能否向下滑动,如果还能滑动,则不拦截事件,将事件传递给 TargetView。如果点击在Header区域,则不拦截事件,将事件传递给地图 MapView。

        @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

    // 如果上次滚动还未结束,则先停下
    if (!mScroller.isFinished())
    mScroller.forceFinished(true);

    // 不拦截事件,将事件传递给TargetView
    if (canChildScrollDown())
    return false;

    int action = event.getAction();

    switch (action) {
    case MotionEvent.ACTION_DOWN:
    mDownY = event.getY();
    mIsDragging = false;
    // 如果点击在Header区域,则不拦截事件
    isDownInTop = mDownY <= mTargetCurrTop - mTouchSlop;
    break;

    case MotionEvent.ACTION_MOVE:
    final float y = event.getY();
    if (isDownInTop) {
    return false;
    } else {
    startDragging(y);
    }

    break;

    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
    mIsDragging = false;
    break;
    }

    return mIsDragging;
    }

    当 CompNsViewGroup 拦截事件后,会调用自身的 onTouchEvent 方法,逻辑与 onInterceptTouchEvent 类似,这里需要注意的是,当事件在ViewGroup内,我们要怎么手动分发给TargetView呢?代码见下:

        @Override
    public boolean onTouchEvent(MotionEvent event) {

    if (canChildScrollDown())
    return false;

    // 添加速度监听
    acquireVelocityTracker(event);

    int action = event.getAction();

    switch (action) {
    case MotionEvent.ACTION_DOWN:
    mIsDragging = false;
    break;

    case MotionEvent.ACTION_MOVE:
    final float y = event.getY();
    startDragging(y);

    if (mIsDragging) {
    float dy = y - mLastMotionY;
    if (dy >= 0) {
    moveTargetView(dy);
    } else {
    /**
    * 此时,事件在ViewGroup内,
    * 需手动分发给TargetView
    */
    if (mTargetCurrTop + dy <= 0) {
    moveTargetView(dy);
    int oldAction = event.getAction();
    event.setAction(MotionEvent.ACTION_DOWN);
    dispatchTouchEvent(event);
    event.setAction(oldAction);
    } else {
    moveTargetView(dy);
    }
    }
    mLastMotionY = y;
    }
    break;

    case MotionEvent.ACTION_UP:
    if (mIsDragging) {
    mIsDragging = false;
    mVelocityTracker.computeCurrentVelocity(500, maxFlingVelocity);
    final float vy = mVelocityTracker.getYVelocity();
    // 滚动的像素数太大了,这里只滚动像素数的0.1
    vyPxCount = (int)(vy/3);
    finishDrag(vyPxCount);
    }
    releaseVelocityTracker();
    return false;

    case MotionEvent.ACTION_CANCEL:
    // 回收滑动监听
    releaseVelocityTracker();
    return false;

    }

    return mIsDragging;
    }

    通过 canChildScrollDown 方法,我们能够判断 RecyclerView 是否能够向下滑动。这里后续会抽出一个adapter类,来处理不同的滑动控件。

        /**
    * 由TargetView来处理滑动事件。
    *
    * <p>注意{@link RecyclerView#canScrollVertically}
    * 来判断当前视图是否可以继续滚动。
    * <ul>
    * <li>正数:实际是判断手指能否向上滑动
    * <li>负数:实际是判断手指能否向下滑动
    * </ul>
    */
    public boolean canChildScrollDown() {
    RecyclerView rv;
    // 当前只做了RecyclerView的适配
    if (mInnerScrollView instanceof RecyclerView) {
    rv = (RecyclerView) mInnerScrollView;
    if (android.os.Build.VERSION.SDK_INT < 14) {
    RecyclerView.LayoutManager lm = rv.getLayoutManager();
    boolean isFirstVisible;
    if (lm != null && lm instanceof LinearLayoutManager) {
    isFirstVisible = ((LinearLayoutManager)lm)
    .findFirstVisibleItemPosition() > 0;
    return rv.getChildCount() > 0
    && (isFirstVisible || rv.getChildAt(0)
    .getTop() < rv.getPaddingTop());
    }
    } else {
    return rv.canScrollVertically(-1);
    }
    }
    return false;
    }

    获取向上能够滑动的距离顶部距离,如果Item数量太少,导致rv不能占满一屏时,注意向上滑动的距离。

        public int toTopMaxOffset() {
    final RecyclerView rv;
    if (mInnerScrollView instanceof RecyclerView) {
    rv = (RecyclerView) mInnerScrollView;
    if (android.os.Build.VERSION.SDK_INT >= 14) {

    return Math.max(0, mTargetInitTop -
    (rv.computeVerticalScrollRange() - mTargetInitBottom));
    }
    }
    return 0;
    }

    手指向下滑动或 TargetView 距离顶部距离 > 0,则 ViewGroup 拦截事件。

        private void startDragging(float y) {
    if (y > mDownY || mTargetCurrTop > toTopMaxOffset()) {
    final float yDiff = Math.abs(y - mDownY);
    if (yDiff > mTouchSlop && !mIsDragging) {
    mLastMotionY = mDownY + mTouchSlop;
    mIsDragging = true;
    }
    }
    }

    这是获取 TargetView 和 HeaderView 顶部距离的方法,我们通过不断刷新顶部距离来实现滑动的效果。

        private void moveTargetViewTo(int target) {
    target = Math.max(target, toTopMaxOffset());
    if (target >= mTargetInitTop)
    target = mTargetInitTop;
    // TargetView的top、bottom两个方向都是加上offsetY
    ViewCompat.offsetTopAndBottom(mTargetView, target - mTargetCurrTop);
    // 更新当前TargetView距离顶部高度H
    mTargetCurrTop = target;

    int headerTarget;
    // 下拉超过定值H
    if (mTargetCurrTop >= mTargetInitTop) {
    headerTarget = mHeaderInitTop;
    } else if (mTargetCurrTop <= 0) {
    headerTarget = 0;
    } else {
    // 滑动比例
    float percent = mTargetCurrTop * 1.0f / mTargetInitTop;
    headerTarget = (int) (percent * mHeaderInitTop);
    }
    // HeaderView的top、bottom两个方向都是加上offsetY
    ViewCompat.offsetTopAndBottom(mHeaderView, headerTarget - mHeaderCurrTop);
    mHeaderCurrTop = headerTarget;

    if (mListener != null) {
    mListener.onTargetToTopDistance(mTargetCurrTop);
    mListener.onHeaderToTopDistance(mHeaderCurrTop);
    }
    }

    这是 mScroller 弹性滑动时的一些阈值判断。startScroll 本身并没有做任何滑动相关的事,而是通过 invalidate 方法来实现 View 重绘,在 View 的 draw 方法中会调用 computeScroll 方法,但本例中并没有在computeScroll 中配合 scrollTo 来实现滑动。注意这里的滑动,是指内容的滑动,而非 View 本身位置的滑动。

        private void finishDrag(int vyPxCount) {
    if ((vyPxCount >= 0 && vyPxCount <= minFlingVelocity)
    || (vyPxCount <= 0 && vyPxCount >= -minFlingVelocity))
    return;

    // 速度 > 0,说明正向下滚动
    if (vyPxCount > 0) {
    // 防止超出临界值
    if (mTargetCurrTop < mTargetInitTop) {
    mScroller.startScroll(0, mTargetCurrTop
    , 0, vyPxCount < (mTargetInitTop - mTargetCurrTop)
    ? vyPxCount : (mTargetInitTop - mTargetCurrTop)
    , 650);
    invalidate();
    }
    }
    // 速度 < 0,说明正向上滚动
    else if (vyPxCount < 0) {
    if (mTargetCurrTop <= 0) {
    if (mScroller.getCurrVelocity() > 0) {
    // inner scroll 接着滚动
    }
    }

    mScroller.startScroll(0, mTargetCurrTop
    , 0, vyPxCount > -mTargetCurrTop
    ? vyPxCount : -mTargetCurrTop
    , 650);
    invalidate();
    }
    }

    在 View 重绘后,computeScroll 方法就会被调用,这里通过更新此时 TargetView 和 HeaderView 的顶部距离,来实现滑动到新的位置的目的。

        @Override
    public void computeScroll() {
    // 判断是否完成滚动,true:未结束
    if (mScroller.computeScrollOffset()) {
    moveTargetViewTo(mScroller.getCurrY());
    invalidate();
    }
    }

    gitHub - CompNestedSlidet

    原创:https://www.panoramacn.com
    源码网提供WordPress源码,帝国CMS源码discuz源码,微信小程序,小说源码,杰奇源码,thinkphp源码,ecshop模板源码,微擎模板源码,dede源码,织梦源码等。

    专业搭建小说网站,小说程序,杰奇系列,微信小说系列,app系列小说

    Android 仿滴滴首页嵌套滑动效果

    免责声明,若由于商用引起版权纠纷,一切责任均由使用者承担。

    您必须遵守我们的协议,如果您下载了该资源行为将被视为对《免责声明》全部内容的认可-> 联系客服 投诉资源
    www.panoramacn.com资源全部来自互联网收集,仅供用于学习和交流,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。 敬请谅解! 侵权删帖/违法举报/投稿等事物联系邮箱:2640602276@qq.com
    未经允许不得转载:书荒源码源码网每日更新网站源码模板! » Android 仿滴滴首页嵌套滑动效果
    关注我们小说电影免费看
    关注我们,获取更多的全网素材资源,有趣有料!
    120000+人已关注
    分享到:
    赞(0) 打赏
  • 评论抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址

    您的打赏就是我分享的动力!

    支付宝扫一扫打赏

    微信扫一扫打赏