<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Android仿滴滴首頁嵌套滑動(dòng)效果

          共 14162字,需瀏覽 29分鐘

           ·

          2021-04-25 12:50

          效果



          在說代碼之前,可以先看下最終的 CompNsViewGroup XML 結(jié)構(gòu),CompNsViewGroup 內(nèi)部包含頂部地圖 MapView 和滑動(dòng)布局 LinearLayout,而 LinearLayout 布局的內(nèi)部即我們常用的滑動(dòng)控件 RecyclerView,在這里為何還要加層 LinearLayout 呢?這樣做的好處是,我們可以更好的適配不同滑動(dòng)控件,而不僅僅是將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>


          實(shí)現(xiàn)


          在 attrs.xml 文件下為 CompNsViewGroup 添加自定義屬性,其中 header_id 對應(yīng)頂部地圖 MapView,target_id 對應(yīng)滑動(dòng)布局 LinearLayout,inn_id 對應(yīng)滑動(dòng)控件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>


          我們根據(jù) attrs.xml 中的屬性,獲取 XML 中 CompNsViewGroup 中的 View ID

                  // 獲取配置參數(shù)        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");


          我們根據(jù) attrs.xml 中的屬性,來初始化 View 的高度、距離等,計(jì)算高度時(shí),需要考慮到狀態(tài)欄因素

                  mHeaderInitTop = Utils.dip2px(getContext()                , array.getInt(R.styleable.CompNsViewGroup_header_init_top, 0));        mHeaderCurrTop = mHeaderInitTop;        // 屏幕高度 - 底部距離 - 狀態(tài)欄高度        mTargetInitBottom = Utils.dip2px(getContext()                , array.getInt(R.styleable.CompNsViewGroup_target_init_bottom, 0));        // 注意:當(dāng)前activity默認(rèn)去掉了標(biāo)題欄        mTargetInitTop = Utils.getScreenHeight(getContext()) - mTargetInitBottom                - Utils.getStatusBarHeight(getContext().getApplicationContext());        mTargetCurrTop = mTargetInitTop;


          通過上面獲取到的 View ID,我們能夠直接引用到 XML 中的相關(guān) View 實(shí)例,而后續(xù)的滑動(dòng),本質(zhì)上就是針對該 View 所進(jìn)行的一系列判斷處理。

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


          我們重寫 onMeasure 方法,其不僅是給 childView 傳入測量值和測量模式,還將我們自己測量的尺寸提供給父 ViewGroup 讓其給我們提供期望大小的區(qū)域。

              @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          // 計(jì)算子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 挪動(dòng)到了屏幕下方看不到的地方。

              @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());    }


          此功能實(shí)現(xiàn)的核心即事件的分發(fā)和攔截了。在接收到事件時(shí),如果上次滾動(dòng)還未結(jié)束,則先停下。隨后判斷TargetView 內(nèi)的 RecyclerView 能否向下滑動(dòng),如果還能滑動(dòng),則不攔截事件,將事件傳遞給 TargetView。如果點(diǎn)擊在Header區(qū)域,則不攔截事件,將事件傳遞給地圖 MapView。

              @Override    public boolean onInterceptTouchEvent(MotionEvent event) {
          // 如果上次滾動(dòng)還未結(jié)束,則先停下 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; // 如果點(diǎn)擊在Header區(qū)域,則不攔截事件 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;    }


          當(dāng) CompNsViewGroup 攔截事件后,會(huì)調(diào)用自身的 onTouchEvent 方法,邏輯與 onInterceptTouchEvent 類似,這里需要注意的是,當(dāng)事件在ViewGroup內(nèi),我們要怎么手動(dòng)分發(fā)給TargetView呢?代碼見下:

              @Override    public boolean onTouchEvent(MotionEvent event) {
          if (canChildScrollDown()) return false;
          // 添加速度監(jiān)聽 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 { /** * 此時(shí),事件在ViewGroup內(nèi), * 需手動(dòng)分發(fā)給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(); // 滾動(dòng)的像素?cái)?shù)太大了,這里只滾動(dòng)像素?cái)?shù)的0.1 vyPxCount = (int)(vy/3); finishDrag(vyPxCount); } releaseVelocityTracker(); return false;
          case MotionEvent.ACTION_CANCEL: // 回收滑動(dòng)監(jiān)聽 releaseVelocityTracker(); return false;
          }
          return mIsDragging;    }


          通過 canChildScrollDown 方法,我們能夠判斷 RecyclerView 是否能夠向下滑動(dòng)。這里后續(xù)會(huì)抽出一個(gè)adapter類,來處理不同的滑動(dòng)控件。

              /**     * 由TargetView來處理滑動(dòng)事件。     *     * <p>注意{@link RecyclerView#canScrollVertically}     * 來判斷當(dāng)前視圖是否可以繼續(xù)滾動(dòng)。     * <ul>     * <li>正數(shù):實(shí)際是判斷手指能否向上滑動(dòng)     * <li>負(fù)數(shù):實(shí)際是判斷手指能否向下滑動(dòng)     * </ul>     */    public boolean canChildScrollDown() {        RecyclerView rv;        // 當(dāng)前只做了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;    }


          獲取向上能夠滑動(dòng)的距離頂部距離,如果Item數(shù)量太少,導(dǎo)致rv不能占滿一屏?xí)r,注意向上滑動(dòng)的距離。

              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;    }


          手指向下滑動(dòng)或 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 頂部距離的方法,我們通過不斷刷新頂部距離來實(shí)現(xiàn)滑動(dòng)的效果。

              private void moveTargetViewTo(int target) {        target = Math.max(target, toTopMaxOffset());        if (target >= mTargetInitTop)            target = mTargetInitTop;        // TargetView的top、bottom兩個(gè)方向都是加上offsetY        ViewCompat.offsetTopAndBottom(mTargetView, target - mTargetCurrTop);        // 更新當(dāng)前TargetView距離頂部高度H        mTargetCurrTop = target;
          int headerTarget; // 下拉超過定值H if (mTargetCurrTop >= mTargetInitTop) { headerTarget = mHeaderInitTop; } else if (mTargetCurrTop <= 0) { headerTarget = 0; } else { // 滑動(dòng)比例 float percent = mTargetCurrTop * 1.0f / mTargetInitTop; headerTarget = (int) (percent * mHeaderInitTop); } // HeaderView的top、bottom兩個(gè)方向都是加上offsetY ViewCompat.offsetTopAndBottom(mHeaderView, headerTarget - mHeaderCurrTop); mHeaderCurrTop = headerTarget;
          if (mListener != null) { mListener.onTargetToTopDistance(mTargetCurrTop); mListener.onHeaderToTopDistance(mHeaderCurrTop); }    }


          這是 mScroller 彈性滑動(dòng)時(shí)的一些閾值判斷。startScroll 本身并沒有做任何滑動(dòng)相關(guān)的事,而是通過 invalidate 方法來實(shí)現(xiàn) View 重繪,在 View 的 draw 方法中會(huì)調(diào)用 computeScroll 方法,但本例中并沒有在computeScroll 中配合 scrollTo 來實(shí)現(xiàn)滑動(dòng)。注意這里的滑動(dòng),是指內(nèi)容的滑動(dòng),而非 View 本身位置的滑動(dòng)。

              private void finishDrag(int vyPxCount) {        if ((vyPxCount >= 0 && vyPxCount <= minFlingVelocity)                || (vyPxCount <= 0 && vyPxCount >= -minFlingVelocity))            return;
          // 速度 > 0,說明正向下滾動(dòng) if (vyPxCount > 0) { // 防止超出臨界值 if (mTargetCurrTop < mTargetInitTop) { mScroller.startScroll(0, mTargetCurrTop , 0, vyPxCount < (mTargetInitTop - mTargetCurrTop) ? vyPxCount : (mTargetInitTop - mTargetCurrTop) , 650); invalidate(); } } // 速度 < 0,說明正向上滾動(dòng) else if (vyPxCount < 0) { if (mTargetCurrTop <= 0) { if (mScroller.getCurrVelocity() > 0) { // inner scroll 接著滾動(dòng) } }
          mScroller.startScroll(0, mTargetCurrTop , 0, vyPxCount > -mTargetCurrTop ? vyPxCount : -mTargetCurrTop , 650); invalidate(); }    }


          在 View 重繪后,computeScroll 方法就會(huì)被調(diào)用,這里通過更新此時(shí) TargetView 和 HeaderView 的頂部距離,來實(shí)現(xiàn)滑動(dòng)到新的位置的目的。

              @Override    public void computeScroll() {        // 判斷是否完成滾動(dòng),true:未結(jié)束        if (mScroller.computeScrollOffset()) {            moveTargetViewTo(mScroller.getCurrY());            invalidate();        }    }


          源碼地址:

          https://codechina.csdn.net/mirrors/MingJieZuo/CustomWidget


          到這里就結(jié)束啦.


          瀏覽 60
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人人超碰人人操 | 香蕉福利在线观看 | 久草天堂 | 北条麻妃影音先锋 | 午夜爱爱影院 |