模仿淘寶商品詳情頁(yè)踩官,實(shí)現(xiàn)上下兩個(gè)FrameLayout切換却桶,并解決在兩個(gè)Fragment中使用ScrollView沖突解決

實(shí)現(xiàn)淘寶詳情頁(yè)上下兩個(gè)界面彈性切換。在此做個(gè)筆記蔗牡,可以直接復(fù)制使用颖系。

    import android.annotation.SuppressLint;
    import android.content.Context;
    import android.support.v4.view.GestureDetectorCompat;
    import android.support.v4.view.ViewCompat;
    import android.support.v4.widget.ViewDragHelper;
    import android.util.AttributeSet;
    import android.view.GestureDetector.SimpleOnGestureListener;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * 這是一個(gè)viewGroup容器,實(shí)現(xiàn)上下兩個(gè)frameLayout拖動(dòng)切換
     *
     * @author ncj
     */
    @SuppressLint("NewApi")
    public class DragLayout extends ViewGroup {

        /* 拖拽工具類 */
        private final ViewDragHelper mDragHelper;
        private GestureDetectorCompat gestureDetector;

        /* 上下兩個(gè)frameLayout辩越,在Activity中注入fragment */
        private View frameView1, frameView2;
        private int viewHeight;
        private static final int VEL_THRESHOLD = 100; // 滑動(dòng)速度的閾值嘁扼,超過(guò)這個(gè)絕對(duì)值認(rèn)為是上下
        private static final int DISTANCE_THRESHOLD = 100; // 單位是像素,當(dāng)上下滑動(dòng)速度不夠時(shí)黔攒,通過(guò)這個(gè)        閾        值來(lái)判定是應(yīng)該粘到頂部還是底部
        private int downTop1; // 手指按下的時(shí)候趁啸,frameView1的getTop值
        private ShowNextPageNotifier nextPageListener; // 手指松開(kāi)是否加載下一頁(yè)的notifier

        public DragLayout(Context context) {
            this(context, null);
        }

        public DragLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }

        public DragLayout(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            mDragHelper = ViewDragHelper
                    .create(this, 10f, new DragHelperCallback());
            mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM);
            gestureDetector = new GestureDetectorCompat(context,
                    new YScrollDetector());
        }

        @Override
        protected void onFinishInflate() {
            // 跟findviewbyId一樣强缘,初始化上下兩個(gè)view
            frameView1 = getChildAt(0);
            frameView2 = getChildAt(1);
        }

        class YScrollDetector extends SimpleOnGestureListener {

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx,
                            float dy) {
        // 垂直滑動(dòng)時(shí)dy>dx,才被認(rèn)定是上下拖動(dòng)
        return Math.abs(dy) > Math.abs(dx);
    }
}

        @Override
        public void computeScroll() {
            if (mDragHelper.continueSettling(true)) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }

        /**
         * 這是拖拽效果的主要邏輯
         */
        private class DragHelperCallback extends ViewDragHelper.Callback {

            @Override
            public void onViewPositionChanged(View changedView, int left, int top,
                                              int dx, int dy) {
                int childIndex = 1;
                if (changedView == frameView2) {
                    childIndex = 2;
                }

                // 一個(gè)view位置改變不傅,另一個(gè)view的位置要跟進(jìn)
                onViewPosChanged(childIndex, top);
            }

            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                // 兩個(gè)子View都需要跟蹤欺旧,返回true
                return true;
            }

            @Override
            public int getViewVerticalDragRange(View child) {
                // 這個(gè)用來(lái)控制拖拽過(guò)程中松手后,自動(dòng)滑行的速度蛤签,暫時(shí)給一個(gè)隨意的數(shù)值
                return 1;
            }

            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                // 滑動(dòng)松開(kāi)后辞友,需要向上或者鄉(xiāng)下粘到特定的位置
                animTopOrBottom(releasedChild, yvel);
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                int finalTop = top;
                if (child == frameView1) {
                    // 拖動(dòng)的時(shí)第一個(gè)view
                    if (top > 0) {
                        // 不讓第一個(gè)view往下拖,因?yàn)轫敳繒?huì)白板
                        finalTop = 0;
                    }
                } else if (child == frameView2) {
                    // 拖動(dòng)的時(shí)第二個(gè)view
                    if (top < 0) {
                             // 不讓第二個(gè)view網(wǎng)上拖震肮,因?yàn)榈撞繒?huì)白板
                        finalTop = 0;
                    }
                }

                // finalTop代表的是理論上應(yīng)該拖動(dòng)到的位置称龙。此處計(jì)算拖動(dòng)的距離除以一個(gè)參數(shù)(3),是讓滑動(dòng)的速度變慢戳晌。數(shù)值越大鲫尊,滑動(dòng)的越慢
                return child.getTop() + (finalTop - child.getTop()) / 3;
            }
        }

        /**
         * 滑動(dòng)時(shí)view位置改變協(xié)調(diào)處理
         *
         * @param viewIndex
         *            滑動(dòng)view的index(1或2)
         * @param posTop
         *            滑動(dòng)View的top位置
         */
        private void onViewPosChanged(int viewIndex, int posTop) {
            if (viewIndex == 1) {
                int offsetTopBottom = viewHeight + frameView1.getTop()
                        - frameView2.getTop();
                frameView2.offsetTopAndBottom(offsetTopBottom);
            } else if (viewIndex == 2) {
                int offsetTopBottom = frameView2.getTop() - viewHeight
                        - frameView1.getTop();
                        frameView1.offsetTopAndBottom(offsetTopBottom);
            }

            // 有的時(shí)候會(huì)默認(rèn)白板,這個(gè)很惡心沦偎。后面有時(shí)間再優(yōu)化
            invalidate();
        }

        private void animTopOrBottom(View releasedChild, float yvel) {
            int finalTop = 0; // 默認(rèn)是粘到最頂端
            if (releasedChild == frameView1) {
                // 拖動(dòng)第一個(gè)view松手
                if (yvel < -VEL_THRESHOLD
                        || (downTop1 == 0 && frameView1.getTop() < -DISTANCE_THRESHOLD)) {
                    // 向上的速度足夠大疫向,就滑動(dòng)到頂端
                    // 向上滑動(dòng)的距離超過(guò)某個(gè)閾值,就滑動(dòng)到頂端
                    finalTop = -viewHeight;

                    // 下一頁(yè)可以初始化了
                    if (null != nextPageListener) {
                        nextPageListener.onDragNext();
                    }
                }
            } else {
                // 拖動(dòng)第二個(gè)view松手
                if (yvel > VEL_THRESHOLD
                || (downTop1 == -viewHeight && releasedChild.getTop() > DISTANCE_THRESHOLD)) {
                    // 保持原地不動(dòng)
                    finalTop = viewHeight;
                }
            }

            if (mDragHelper.smoothSlideViewTo(releasedChild, 0, finalTop)) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }

        /* touch事件的攔截與處理都交給mDraghelper來(lái)處理 */
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {

            if (frameView1.getBottom() > 0 && frameView1.getTop() < 0) {
                // view粘到頂部或底部豪嚎,正在動(dòng)畫(huà)中的時(shí)候搔驼,不處理touch事件
                return false;
            }

            boolean yScroll = gestureDetector.onTouchEvent(ev);
            boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);
            int action = ev.getActionMasked();

            if (action == MotionEvent.ACTION_DOWN) {
                // action_down時(shí)就讓mDragHelper開(kāi)始工作,否則有時(shí)候?qū)е庐惓?他大爺?shù)?                mDragHelper.processTouchEvent(ev);
                downTop1 = frameView1.getTop();
            }

            return shouldIntercept && yScroll;
        }

        @Override
        public boolean onTouchEvent(MotionEvent e) {
            // 統(tǒng)一交給mDragHelper處理侈询,由DragHelperCallback實(shí)現(xiàn)拖動(dòng)效果
            mDragHelper.processTouchEvent(e); // 該行代碼可能會(huì)拋異常舌涨,正式發(fā)布時(shí)請(qǐng)將這行代碼加上try catch
            return true;
        }

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            // 只在初始化的時(shí)候調(diào)用
            // 一些參數(shù)作為全局變量保存起來(lái)
            frameView1.layout(l, 0, r, b - t);
            frameView2.layout(l, 0, r, b - t);

            viewHeight = frameView1.getMeasuredHeight();
            frameView2.offsetTopAndBottom(viewHeight);
        }

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

            int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
            int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
            setMeasuredDimension(
                    resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
                    resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
        }

        /**
         * 這是View的方法,該方法不支持android低版本(2.2扔字、2.3)的操作系統(tǒng)囊嘉,所以手動(dòng)復(fù)制過(guò)來(lái)以免強(qiáng)制退出
         */
        public static int resolveSizeAndState(int size, int measureSpec,
                                              int childMeasuredState) {
            int result = size;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
            switch (specMode) {
                case MeasureSpec.UNSPECIFIED:
                    result = size;
                    break;
                case MeasureSpec.AT_MOST:
                    if (specSize < size) {
                        result = specSize | MEASURED_STATE_TOO_SMALL;
                    } else {
                        result = size;
                   }
                    break;
                case MeasureSpec.EXACTLY:
                    result = specSize;
                    break;
            }
            return result | (childMeasuredState & MEASURED_STATE_MASK);
        }

        public void setNextPageListener(ShowNextPageNotifier nextPageListener) {
            this.nextPageListener = nextPageListener;
        }

        public interface ShowNextPageNotifier {
            public void onDragNext();
        }
    }

布局中使用:

    <DragLayout
        android:id="@+id/slideLl"
         android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:id="@+id/one"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

        <FrameLayout
            android:id="@+id/two"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </DragLayout>

在Activity中使用:

/**

* 框架
*/
public class DragActivity extends BaseActivity implements ShowNextPageNotifier {
     @BindView(R.id.dl_layout)
     DragLayout dragLayout;

     private FragmentProductOne mOneFrament;
     private FragmentProductTwo mTwoFrament;

    @Override
    protected int getResId() {
        return R.layout.activity_product_detail;
    }

    @Override
    protected void initView() {
        mOneFrament = new FragmentProductOne();
        mTwoFrament = new FragmentProductTwo();
        getSupportFragmentManager().beginTransaction()
                .add(R.id.fy_fragment_one, mOneFrament)
                .add(R.id.fy_fragment_two, mTwoFrament).commit();
        dragLayout.setNextPageListener(this);
    }




    @Override
  public void onDragNext() {
  }
}

到這里不算完事,大家可以看到淘寶頁(yè)會(huì)有兩個(gè)能滑動(dòng)的界面革为,使用的布局中存在著ScrollView扭粱,這樣就會(huì)有滑動(dòng)手勢(shì)沖突,在這里給大家自定義一個(gè)使用的ScrollView 能完美解決這個(gè)問(wèn)題:

    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.ViewConfiguration;
    import android.widget.ScrollView;

    /**
     * Created by ncj on 2018/2/6.
     */

    public class DragScrollView extends ScrollView {
        private static final int TOUCH_IDLE = 0;
        private static final int TOUCH_INNER_CONSIME = 1; // touch事件由ScrollView內(nèi)部消費(fèi)
        private static final int TOUCH_DRAG_LAYOUT = 2; // touch事件由上層的DragLayout去消費(fèi)

        public static final int TOP_MODEL = 1;
        public static final int BOTTOM_MODE = 2;

        private int model = 1;
        boolean isAtBottom; // 按下的時(shí)候是否在底部
        boolean isAtTop;    // 按下的時(shí)候是否在頂部
        private int mTouchSlop = 4; // 判定為滑動(dòng)的閾值震檩,單位是像素
        private int scrollMode;
        private float downY;

        public DragScrollView(Context arg0) {
            this(arg0, null);
        }

        public DragScrollView(Context arg0, AttributeSet arg1) {
            this(arg0, arg1, 0);
        }

        public DragScrollView(Context arg0, AttributeSet arg1, int arg2) {
            super(arg0, arg1, arg2);
            ViewConfiguration configuration = ViewConfiguration.get(getContext());
            mTouchSlop = configuration.getScaledTouchSlop();
        }

        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();

            //子View一定要Clickable才行琢蛤,否則onInterceptTouchEvent工作不按正常來(lái)
            getChildAt(0).setClickable(true);
        }

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                downY = ev.getRawY();
                isAtBottom = isAtBottom();
                isAtTop = isAtTop();
                scrollMode = TOUCH_IDLE;
                getParent().requestDisallowInterceptTouchEvent(true);
            } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
                if (scrollMode == TOUCH_IDLE) {
                    float yOffset = downY - ev.getRawY();
                    float yDistance = Math.abs(yOffset);
                    if (yDistance > mTouchSlop) {
                        if (yOffset > 0 && isAtBottom && (model == BOTTOM_MODE)) {
                            scrollMode = TOUCH_DRAG_LAYOUT;
                            getParent().requestDisallowInterceptTouchEvent(false);
                            return true;
                        } else if (yOffset < 0 && isAtTop && (model == TOP_MODEL)) {
                            scrollMode = TOUCH_DRAG_LAYOUT;
                            getParent().requestDisallowInterceptTouchEvent(false);
                            return true;
                        } else {
                            scrollMode = TOUCH_INNER_CONSIME;
                        }
                    }
                }
            }
            return super.onInterceptTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (scrollMode == TOUCH_DRAG_LAYOUT) {
                return false;
            }
            return super.onTouchEvent(ev);
        }

        private boolean isAtBottom() {
            return getScrollY() + getMeasuredHeight() >= computeVerticalScrollRange() - 2;
        }

        private boolean isAtTop() {
            return getScrollY() == 0;
        }

        public void setMode(int model) {
            this.model = model;
        }

    }

DragScrollView可以直接在布局中使用,使用方式為:

    <com.ncj.mvp.demo.view.DragScrollView
            android:id="@+id/drag_scroll_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="none">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">
                //你的內(nèi)容
            </LinearLayout>
     </com.ncj.mvp.demo.view.DragScrollView>

使用的時(shí)候在第一個(gè)Fragment里面添加

    @BindView(R.id.drag_scroll_view)
      DragScrollView scrollView;

初始化后添加:

    scrollView.setMode(DragScrollView.BOTTOM_MODE);

這樣就能解決兩個(gè)上下頁(yè)面有滑動(dòng)事件的沖突問(wèn)題!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恳蹲,一起剝皮案震驚了整個(gè)濱河市虐块,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嘉蕾,老刑警劉巖贺奠,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異错忱,居然都是意外死亡儡率,警方通過(guò)查閱死者的電腦和手機(jī)挂据,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)儿普,“玉大人崎逃,你說(shuō)我怎么就攤上這事∶己ⅲ” “怎么了个绍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)浪汪。 經(jīng)常有香客問(wèn)我巴柿,道長(zhǎng),這世上最難降的妖魔是什么死遭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任广恢,我火速辦了婚禮,結(jié)果婚禮上呀潭,老公的妹妹穿的比我還像新娘钉迷。我一直安慰自己,他們只是感情好钠署,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布糠聪。 她就那樣靜靜地躺著,像睡著了一般踏幻。 火紅的嫁衣襯著肌膚如雪枷颊。 梳的紋絲不亂的頭發(fā)上戳杀,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天该面,我揣著相機(jī)與錄音,去河邊找鬼信卡。 笑死隔缀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的傍菇。 我是一名探鬼主播猾瘸,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼丢习!你這毒婦竟也來(lái)了牵触?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤咐低,失蹤者是張志新(化名)和其女友劉穎揽思,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體见擦,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钉汗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年羹令,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片损痰。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡福侈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卢未,到底是詐尸還是另有隱情肪凛,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布辽社,位于F島的核電站显拜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏爹袁。R本人自食惡果不足惜远荠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望失息。 院中可真熱鬧譬淳,春花似錦、人聲如沸盹兢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)绎秒。三九已至浦妄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間见芹,已是汗流浹背剂娄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留玄呛,地道東北人阅懦。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像徘铝,于是被迫代替她去往敵國(guó)和親耳胎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355