MaterialDesign--(10)CoordinatorLayout和 Behavior 的源碼分析及使用

CoordinatorLayout

終于到這個(gè)控件了格郁,其實(shí)我的內(nèi)心是忐忑的嗓奢,因?yàn)槲移鋵?shí)一直想要深入的理解 CoordinatorLayout+Behavior的原理豌汇,但是又苦于太難懂了手销,以前也零零碎碎研究過(guò)幾次宋彼,最后都以失敗告終睦裳。這次是沒(méi)辦法造锅,MaterialDesign 篇到這里也快結(jié)束了,做事還是要有始有終廉邑,于是這兩天好好研究了一下哥蔚,發(fā)現(xiàn)這東西其實(shí)也沒(méi)那么復(fù)雜。

CoordinatorLayout直接繼承了ViewGroup蛛蒙,說(shuō)明最少重寫(xiě)了 onMeasure和 onLayout 方法糙箍,說(shuō)不定還有 onMeasureChild和 onLayoutChild 方法。然后我們?cè)诳辞K睿珻oordinatorLayout 是用來(lái)處理嵌套滑動(dòng)的深夯,那么onTouchEvent()和 onInterceptTouchEvent()方法肯定也跑不掉。

好了诺苹,按照慣例咕晋,我們先從構(gòu)造方法以及 attributes 屬性開(kāi)始看吧。

attributes

<declare-styleable name="CoordinatorLayout">
    <attr format="reference" name="keylines"/>
    <attr format="reference" name="statusBarBackground"/>
</declare-styleable>

<declare-styleable name="CoordinatorLayout_Layout">
    <attr name="android:layout_gravity"/>
    <attr format="string" name="layout_behavior"/>
    <attr format="reference" name="layout_anchor"/>
    <attr format="integer" name="layout_keyline"/>
    <attr name="layout_anchorGravity">
        <flag name="top" value="0x30"/>            
        <flag name="bottom" value="0x50"/>            
        <flag name="left" value="0x03"/>            
        <flag name="right" value="0x05"/>            
        <flag name="center_vertical" value="0x10"/>            
        <flag name="fill_vertical" value="0x70"/>           
        <flag name="center_horizontal" value="0x01"/>            
        <flag name="fill_horizontal" value="0x07"/>            
        <flag name="center" value="0x11"/>          
        <flag name="fill" value="0x77"/>            
        <flag name="clip_vertical" value="0x80"/>            
        <flag name="clip_horizontal" value="0x08"/>            
        <flag name="start" value="0x00800003"/>            
        <flag name="end" value="0x00800005"/>
    </attr>
    <attr format="enum" name="layout_insetEdge">          
        <enum name="none" value="0x0"/>            
        <enum name="top" value="0x30"/>            
        <enum name="bottom" value="0x50"/>            
        <enum name="left" value="0x03"/>            
        <enum name="right" value="0x03"/>            
        <enum name="start" value="0x00800003"/>            
        <enum name="end" value="0x00800005"/>
    </attr>
    <attr name="layout_dodgeInsetEdges">            
        <flag name="none" value="0x0"/>            
        <flag name="top" value="0x30"/>            
        <flag name="bottom" value="0x50"/>            
        <flag name="left" value="0x03"/>            
        <flag name="right" value="0x03"/>            
        <flag name="start" value="0x00800003"/>            
        <flag name="end" value="0x00800005"/>            
        <flag name="all" value="0x77"/>
    </attr></declare-styleable>

可以直接設(shè)置在 CoordinatorLayout 節(jié)點(diǎn)上的屬性有兩個(gè)

  • keylines 一個(gè)比較奇怪的屬性收奔,好像是一個(gè)布局解決方案吧掌呜。比較雞肋,沒(méi)有人用過(guò)它
  • statusBarBackground 狀態(tài)欄背景顏色

剩下的都是只能作用在 CoordinatorLayout 的直接子節(jié)點(diǎn)上的屬性

  • layout_behavior 這個(gè)屬性大家都很熟悉坪哄,因?yàn)锽ehavior 是嵌套滑動(dòng)的精華质蕉。輔助Coordinator對(duì)View進(jìn)行l(wèi)ayout、nestedScroll的處理
  • layout_anchor 將其固定在某個(gè) view 上面翩肌,可以理解成依附
  • layout_keyline 同上
  • layout_anchorGravity 這個(gè)容易理解模暗,依附在控件上的位置
  • layout_insetEdge 用于避免布局之間互相遮蓋
  • layout_dodgeInsetEdges 用于避免布局之間互相遮蓋

布局這一塊沒(méi)什么說(shuō)的,CoordinatorLayout作為頂級(jí)節(jié)點(diǎn)念祭,然后根據(jù)實(shí)際需求使用對(duì)應(yīng)的控件和屬性就行了兑宇,這里我就不做過(guò)多的贅述。

源碼

首先粱坤,剛剛我們猜測(cè)了肯定會(huì)重寫(xiě) onMeasure 方法顾孽,那么我們就從 onMeasure 方法開(kāi)始看祝钢。
onMeasure 方法里面有這么一段方法,我 copy 出來(lái)給大家看一下

for (int i = 0; i < childCount; i++) {
    final View child = mDependencySortedChildren.get(i);
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    int keylineWidthUsed = 0;
    if (lp.keyline >= 0 && widthMode != MeasureSpec.UNSPECIFIED) {
        final int keylinePos = getKeyline(lp.keyline);
        final int keylineGravity = GravityCompat.getAbsoluteGravity(
                    resolveKeylineGravity(lp.gravity), layoutDirection)
                    & Gravity.HORIZONTAL_GRAVITY_MASK;
        if ((keylineGravity == Gravity.LEFT && !isRtl)
                    || (keylineGravity == Gravity.RIGHT && isRtl)) {
                keylineWidthUsed = Math.max(0, widthSize - paddingRight - keylinePos);
        } else if ((keylineGravity == Gravity.RIGHT && !isRtl)
                    || (keylineGravity == Gravity.LEFT && isRtl)) {
                keylineWidthUsed = Math.max(0, keylinePos - paddingLeft);
        }
    }

    int childWidthMeasureSpec = widthMeasureSpec;
    int childHeightMeasureSpec = heightMeasureSpec;
    if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
        // We're set to handle insets but this child isn't, so we will measure the
        // child as if there are no insets
        final int horizInsets = mLastInsets.getSystemWindowInsetLeft()
                    + mLastInsets.getSystemWindowInsetRight();
        final int vertInsets = mLastInsets.getSystemWindowInsetTop()
                    + mLastInsets.getSystemWindowInsetBottom();

        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                    widthSize - horizInsets, widthMode);
        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    heightSize - vertInsets, heightMode);
    }

    final Behavior b = lp.getBehavior();
    if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
    }

    widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                lp.leftMargin + lp.rightMargin);

    heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                lp.topMargin + lp.bottomMargin);
    childState = ViewCompat.combineMeasuredStates(childState,
                ViewCompat.getMeasuredState(child));
}

這是一段遍歷子 View 的操作若厚,首先判斷 keyLine拦英,這個(gè)屬性我們不關(guān)心,直接跳過(guò)测秸,然后就是獲取子 view 的 Behavior疤估,然后判斷是否為空,在根據(jù) Behavior 去 measure 子 view霎冯。這里我們能看到子 view 的 Behavior 是保存在 LayoutParams里面的铃拇,所以這個(gè) LayoutParams 肯定是重寫(xiě)的。然后我們 Behavior 一般是直接寫(xiě)到 xml 布局的子節(jié)點(diǎn)上對(duì)吧沈撞,所以可以判斷子 view 的 Behavior 是在View 解析 xml 的時(shí)候慷荔,讀取到 Behavior 節(jié)點(diǎn),然后賦值給 LayoutParams缠俺。LayoutInflate 的源碼我就不帶著大家去讀了显晶,我貼出關(guān)鍵代碼

ViewGroup.LayoutParams params = null;
try {
    params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
    // Ignore, just fail over to child attrs.
}

這里的group 就是 parent強(qiáng)轉(zhuǎn)的,子 View 的 LayoutParams 是通過(guò)父 view 的generateLayoutParams()創(chuàng)建壹士,于是我們?nèi)タ?CoordinatorLayout 的generateLayoutParams方法磷雇。

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

額,尷尬了躏救,直接去看構(gòu)造方法把

LayoutParams(Context context, AttributeSet attrs) {
    super(context, attrs);

    final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.CoordinatorLayout_Layout);

    this.gravity = a.getInteger(
                R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
                Gravity.NO_GRAVITY);
    mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
                View.NO_ID);
    this.anchorGravity = a.getInteger(
                R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
                Gravity.NO_GRAVITY);

     this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
                -1);

    insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
        dodgeInsetEdges = a.getInt(
                R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
    mBehaviorResolved = a.hasValue(
                R.styleable.CoordinatorLayout_Layout_layout_behavior);
    if (mBehaviorResolved) {
        mBehavior = parseBehavior(context, attrs, a.getString(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior));
    }
    a.recycle();

    if (mBehavior != null) {
        // If we have a Behavior, dispatch that it has been attached
            mBehavior.onAttachedToLayoutParams(this);
    }
}

好唯笙,這里我們可以看到,我們之前設(shè)置的一些layout_anchor盒使、anchorGravity崩掘、layout_keyline、layout_behavior等屬性少办,我就不過(guò)多贅述了苞慢,今天的重點(diǎn)是 Behavior 呢,我們看parseBehavior()方法凡泣,這個(gè)方法創(chuàng)建了一個(gè) Behavior

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if (TextUtils.isEmpty(name)) {
        return null;
    }

    final String fullName;
    if (name.startsWith(".")) {
        // Relative to the app package. Prepend the app package name.
        fullName = context.getPackageName() + name;
    } else if (name.indexOf('.') >= 0) {
        // Fully qualified package name.
        fullName = name;
    } else {
        // Assume stock behavior in this package (if we have one)
        fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                ? (WIDGET_PACKAGE_NAME + '.' + name)
                : name;
    }

    try {
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        if (c == null) {
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                    context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

可以看到,內(nèi)部是通過(guò)反射的方式創(chuàng)建的Behavior皮假,然后調(diào)用的兩個(gè)參數(shù)的構(gòu)造函數(shù)鞋拟,所以如果想要試用behavior就必須實(shí)現(xiàn)它的構(gòu)造函數(shù),不然就會(huì)報(bào)異常惹资。哈哈贺纲,反正我第一次創(chuàng)建 Behavior 的時(shí)候運(yùn)行時(shí)報(bào)錯(cuò)了,說(shuō)找不到兩個(gè)參數(shù)的構(gòu)造方法褪测。

好猴誊,接下來(lái)開(kāi)始到重點(diǎn)了潦刃。measure 方法結(jié)束了之后,應(yīng)該開(kāi)始布局了懈叹,所以我們解析來(lái)去看 onLayout()方法

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();

        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

遍歷子 view乖杠,如果 behavior.onLayoutChild()方法返回true,則不會(huì)調(diào)用 CoordinatorLayout 的 onLayouChild()方法澄成,由此可得出結(jié)論胧洒,重寫(xiě) Behavior 的 onLayoutChild 方法是用來(lái)自定義當(dāng)前 View 的布局方式

此時(shí)墨状,布局結(jié)束卫漫,我們的 CoordinatorLayout 靜態(tài)頁(yè)面已經(jīng)完成,接下來(lái)肾砂,我們要看的是滑動(dòng)的時(shí)候列赎,CoordinatorLayout 怎么處理。
我們來(lái)簡(jiǎn)單回顧一下 ViewGroup 的事件分發(fā)機(jī)制镐确,首先 disPatchTouchEvent()被調(diào)用包吝,然后調(diào)用 onInterceptTouchEvent 判斷是否允許事件往下傳,如果允許則丟給子 View的disPatchTouchEvent 來(lái)處理辫塌,如果不允許或者允許后子 view沒(méi)有消費(fèi)掉事件漏策,則 先后調(diào)用自己的 onTouchListener 和 OnTouchEvent來(lái)消費(fèi)事件。
然后我們來(lái)根據(jù)這個(gè)順序看 CoordinatorLayout 的事件處理順序臼氨,首先看 disPatchTouchEvent 方法掺喻, 這個(gè)方法,沒(méi)有重寫(xiě)储矩,那么略過(guò)直接看 onInterceptTouchEvent 方法感耙。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    // Make sure we reset in case we had missed a previous important event.
    if (action == MotionEvent.ACTION_DOWN) {
    //重置狀態(tài)
        resetTouchBehaviors();
    }

    final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
    //重置狀態(tài)
        resetTouchBehaviors();
    }

    return intercepted;
}

沒(méi)什么好說(shuō)的,繼續(xù)追performIntercept()方法

private boolean performIntercept(MotionEvent ev, final int type) {
    boolean intercepted = false;
    boolean newBlock = false;

    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    final List<View> topmostChildList = mTempList1;
    getTopSortedChildren(topmostChildList);

    // Let topmost child views inspect first
    final int childCount = topmostChildList.size();
    for (int i = 0; i < childCount; i++) {
        final View child = topmostChildList.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();

        if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
            // Cancel all behaviors beneath the one that intercepted.
            // If the event is "down" then we don't have anything to cancel yet.
            if (b != null) {
                if (cancelEvent == null) {
                    final long now = SystemClock.uptimeMillis();
                    cancelEvent = MotionEvent.obtain(now, now,
                            MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                }
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        b.onInterceptTouchEvent(this, child, cancelEvent);
                        break;
                    case TYPE_ON_TOUCH:
                        b.onTouchEvent(this, child, cancelEvent);
                        break;
                }
            }
            continue;
        }

        if (!intercepted && b != null) {
            switch (type) {
                case TYPE_ON_INTERCEPT:
                    intercepted = b.onInterceptTouchEvent(this, child, ev);
                    break;
                case TYPE_ON_TOUCH:
                    intercepted = b.onTouchEvent(this, child, ev);
                    break;
            }
            if (intercepted) {
                mBehaviorTouchView = child;
            }
        }

        // Don't keep going if we're not allowing interaction below this.
        // Setting newBlock will make sure we cancel the rest of the behaviors.
        final boolean wasBlocking = lp.didBlockInteraction();
        final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
        newBlock = isBlocking && !wasBlocking;
        if (isBlocking && !newBlock) {
            // Stop here since we don't have anything more to cancel - we already did
            // when the behavior first started blocking things below this point.
            break;
        }
    }

    topmostChildList.clear();

    return intercepted;
}

遍歷所有子 View持隧,調(diào)用了符合條件的 view 的 Behavior.onInterceptTouchEvent/onTouchEvent方法
然后我們來(lái)看 onTouchEvent 方法

@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
        // Safe since performIntercept guarantees that
        // mBehaviorTouchView != null if it returns true
        final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }

    // Keep the super implementation correct
    if (mBehaviorTouchView == null) {
        handled |= super.onTouchEvent(ev);
    } else if (cancelSuper) {
        if (cancelEvent == null) {
            final long now = SystemClock.uptimeMillis();
            cancelEvent = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
        }
        super.onTouchEvent(cancelEvent);
    }

    if (!handled && action == MotionEvent.ACTION_DOWN) {

    }

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }

    return handled;
}

重點(diǎn)在if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))這句話上即硼,如果先前有子 view 的Behavior 的 onInterceptTouchEvent 返回了 true,則直接調(diào)用這個(gè)子 view 的 Behavior 的 onTouchEvent屡拨。否則就繼續(xù)走一遍performIntercept(ev, TYPE_ON_TOUCH)只酥,即:執(zhí)行所有含有 Behavior 的子 view 的 Behavior.onTouchEvent方法。

咳咳~~好了呀狼, 上面兩個(gè)方法的各種邏輯判斷有點(diǎn)繞裂允,我也是被繞了很久,沒(méi)看懂沒(méi)事哥艇,直接看杰倫
我們?cè)賮?lái)回過(guò)頭看這兩個(gè)方法绝编,其最終都是調(diào)用了 Behavior 的 onInterceptTouchEvent 和 onTouchEvent 方法,然后各種條件判斷就是什么時(shí)候調(diào)用這兩個(gè)方法。

  • onInterceptTouchEvent
    1.在 CoordinatorLayout 的 onInterceptTouchEvent 方法中杯調(diào)用 十饥。
    2.調(diào)用順序:按照 CoordinatorLayout 中 child 的添加倒敘進(jìn)行調(diào)用
    3.運(yùn)行原理:
    如果此方法在 down 事件返回 true窟勃,那么它后面的 view 的 Behavior 都執(zhí)行不到此方法;并且執(zhí)行 onTouchEvent 事件的時(shí)候只會(huì)執(zhí)行此 view 的 Behavior 的 onTouchEvent 方法逗堵。
    如果不是 down 事件返回 true秉氧,那么它后面的 view 的 Behavior 的 onInterceptTouchEvent 方法都會(huì)執(zhí)行,但還是只執(zhí)行第一個(gè) view 的 Behavior 的 onTouchEvent 方法
    如果所有的 view 的 Behavior 的onInterceptTouchEvent 方法都沒(méi)有返回 true砸捏,那么在 CoordinatorLayout 的 onTouchEvent 方法內(nèi)會(huì)回調(diào)所有 child 的 Behavior 的 onTouchEvent 方法
    4.CoordinatorLayout 的 onInterceptTouchEvent 默認(rèn)返回 false谬运,返回值由child 的 Behavior 的 onInterceptTouchEvent 方法決定

  • onTouchEvent
    1.在 CoordinatorLayout 的 onTouchEvent 方法中被調(diào)用
    2.調(diào)用順序:同上
    3.在上面 onInterceptTouchEvent 提到的所有 Behavior 的 onTouchEvent 都返回 false 的情況下,會(huì)遍歷所有 child 的此方法垦藏,但是只要有一個(gè) Behavior 的此方法返回 true梆暖,那么后面的所有 child 的此方法都不會(huì)執(zhí)行
    4.CoordinatorLayout 的 onTouchEvent默認(rèn)返回super.onTouchEvent(),如果有 child 的 Behavior 的此方法返回 true掂骏,則返回 true轰驳。

然后再來(lái)說(shuō)一下嵌套滑動(dòng)把,我們都知道 CoordinatorLayout 的內(nèi)嵌套滑動(dòng)只能用 NestedScrollView 和 RecyclerView弟灼,至于為什么呢级解。我相信很多人肯定點(diǎn)開(kāi)過(guò) NestedScrollView 和 RecyclerView 的源碼,細(xì)心的同學(xué)肯定會(huì)發(fā)現(xiàn)這兩個(gè)類(lèi)都實(shí)現(xiàn)了NestedScrollingChild接口田绑,而我們的 CoordinatorLayout 則實(shí)現(xiàn)了NestedScrollingParent的接口勤哗。這兩個(gè)接口不是這篇文章的重點(diǎn),我簡(jiǎn)單說(shuō)一下掩驱,CoordinatorLayout 的內(nèi)嵌滑動(dòng)事件都是被它的子NestedScrollingChild實(shí)現(xiàn)類(lèi)處理的芒划。而子View 在滑動(dòng)的時(shí)候,會(huì)調(diào)用NestedScrollingParent的方法欧穴,于是 CoordinatorLayout 再NestedScrollingParent的實(shí)現(xiàn)方法中民逼,調(diào)用了 Behavior 的對(duì)應(yīng)方法。

總結(jié)

好了涮帘,分析到這里拼苍,其實(shí)我感覺(jué),我們更應(yīng)該去了解一下NestedScrollingParent和NestedScrollingChild的嵌套滾動(dòng)機(jī)制调缨。簡(jiǎn)單點(diǎn)說(shuō)疮鲫,就是 child(RecycleView) 在滾動(dòng)的時(shí)候調(diào)用了 parent(CoordinatorLayout) 的 對(duì)應(yīng)方法,而我們的 Behavior弦叶,則是在 parent 的回調(diào)方法中俊犯,處理了其他child 的伴隨變化。
本質(zhì)上湾蔓,我們可以通過(guò)自定義控件的方式實(shí)現(xiàn)瘫析,但是 Google幫我們封裝的這一套控件很解耦啊砌梆、很牛逼啊默责、很方便啊贬循,所以我用它。


Behavior

直接看官網(wǎng)描述吧

  • Interaction behavior plugin for child views of CoordinatorLayout.
    對(duì) CoordinatorLayout 的 child 交互行為的插件桃序。

  • A Behavior implements one or more interactions that a user can take on a child view. These interactions may include drags, swipes, flings, or any other gestures.
    一個(gè)child 的Behavior 可以實(shí)現(xiàn)一個(gè)或者多個(gè) child 的交互杖虾,這些交互可以包括拖動(dòng)、滑動(dòng)媒熊、慣性以及其他手勢(shì)奇适。

按照國(guó)際慣例,我們應(yīng)該先看 Public methods芦鳍。但是嚷往,As a Developer,我們是不需要持有 Behavior 引用的柠衅。所有的 Behavior CoordinatorLayout都已經(jīng)幫我們管理好了皮仁,所以,我們可以先不用關(guān)心公共方法菲宴。

好了贷祈,那我們聊兩點(diǎn)大家需要關(guān)心的。

Dependent

Dependent: adj.依賴的

顧名思義喝峦,Dependent 就是依賴的意思势誊。在 Behavior 中,就是使某個(gè) View依賴一個(gè)指定的 view谣蠢,使得被依賴的 view 的大小位置改變發(fā)生變化的時(shí)候粟耻,依賴的 view 也可以做出相應(yīng)的動(dòng)作。
常見(jiàn)的方法有兩個(gè)

  • public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)

確定所提供的child 是否有另一個(gè)特點(diǎn)兄弟 View 的依賴
在一個(gè)CoordinatorLayout 布局里面漩怎,這個(gè)方法最少會(huì)被調(diào)用一次勋颖,如果對(duì)于一個(gè)給定的 child 和依賴返回 true,則父CoordinatorLayout 將:
1.在被依賴的view Layout 發(fā)生改變后勋锤,這個(gè) child 也會(huì)重新 layout饭玲。
2.當(dāng)依賴關(guān)系視圖的布局或位置變化時(shí),會(huì)調(diào)用 onDependentViewChange

  • public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency)

響應(yīng)依賴 view 變化的方法
無(wú)論是依賴 view的尺寸叁执、大小或者位置發(fā)生改變茄厘,這個(gè)方法都會(huì)被調(diào)用矾湃,一個(gè) Behavior 可以使用此方法來(lái)適當(dāng)?shù)母马憫?yīng)child
view 的依賴關(guān)系由layoutDependsOn 或者child 設(shè)置了another屬性來(lái)確定噪沙。
如果 Behavior 改變了 child 的大小或位置,它應(yīng)該返回 true睬关,默認(rèn)返回 false吆录。

好了窑滞,說(shuō)了這么久,我們來(lái)動(dòng)手寫(xiě)一個(gè)小 Demo 吧。
大家都知道 FloatActionBar 在 CoordinatorLayout 布局中哀卫,處于屏幕底部時(shí)巨坊,然后 SnackBar 彈出來(lái)之后,會(huì)自動(dòng)把 FloatActionBar 頂上去此改。就像醬紫

FloatActionBar.gif

布局文件和代碼都賊簡(jiǎn)單~如下

<android.support.design.widget.CoordinatorLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 tools:context=".TestActivity">

 <android.support.design.widget.FloatingActionButton
    android:id="@+id/bt"
    android:layout_gravity="bottom|right"
    app:layout_behavior=".behavior.FABBehavior"
    android:layout_width="wrap_content"
    android:text="FloatActionBar"
    android:layout_height="wrap_content"/>

</android.support.design.widget.CoordinatorLayout>

findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Snackbar.make(v,"自定義 FloatActionBar", 1500).show();
        }
    });

這里就不一步一步去糾結(jié)了趾撵,F(xiàn)loatingActionButton內(nèi)部有一個(gè)默認(rèn)的 Behavior,這個(gè) Behavior 實(shí)現(xiàn)了很多效果共啃,我們先跳過(guò)吧占调。
然后我們來(lái)自定義一個(gè) FloatingActionButton,實(shí)現(xiàn) SnackBar 彈出的時(shí)候頂上去移剪。

實(shí)現(xiàn)思路:我們要在SnackBar 在彈出的時(shí)候跟著一起往上位移究珊,也就是說(shuō)我們要監(jiān)聽(tīng) SnackBar 的位移事件,那么我們可以layoutDependsOn判斷目前發(fā)生變化的 view 是不是 SnackBar纵苛,如果是苦银,則返回 true,onDependentViewChanged方法赶站,在onDependentViewChanged里面改變 MyFloatingActionButton 的位置幔虏。

我要自己實(shí)現(xiàn)的效果:

MyFloatActionBar.gif

代碼實(shí)現(xiàn),xml 布局(這里我圖方便贝椿,就用一個(gè) Button 代替了想括,樣式也沒(méi)改。烙博。瑟蜈。主要是因?yàn)閼校?/p>

<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".TestActivity">

<Button
    android:id="@+id/bt"
    android:layout_gravity="bottom|right"
    app:layout_behavior=".behavior.FABBehavior"
    android:layout_width="wrap_content"
    android:text="FloatActionBar"
    android:layout_height="wrap_content"/>
</android.support.design.widget.CoordinatorLayout>

這里其實(shí)就是一個(gè) CoordinatorLayout 里面包裹了一個(gè) Button,關(guān)鍵代碼就是“app:layout_behavior=".behavior.FABBehavior"”渣窜,然后我們需要去寫(xiě)這個(gè) FABBehavior 就行了铺根。邏輯很簡(jiǎn)單,代碼如下:

public class FABBehavior extends CoordinatorLayout.Behavior {

public FABBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof Snackbar.SnackbarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
    child.setTranslationY(translationY);
    return true;
}
}

很簡(jiǎn)單的代碼邏輯乔宿,我就不寫(xiě)注釋了位迂,這里記得寫(xiě)兩個(gè)參數(shù)的構(gòu)造方法就行了。

然后我們?cè)賮?lái)看一個(gè)基于 Dependent 的 Demo

BottomBehavior.gif

好了详瑞,直接貼代碼吧掂林,很簡(jiǎn)單的。

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">


    <android.support.v7.widget.Toolbar
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_scrollFlags="scroll|enterAlways"
        app:navigationIcon="@mipmap/abc_ic_ab_back_mtrl_am_alpha"
        app:title="標(biāo)題">

    </android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="Tag"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>


<TextView
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:layout_gravity="bottom"
    android:background="@color/colorPrimary_pink"
    android:gravity="center"
    android:text="我是底部導(dǎo)航欄"
    android:textColor="@color/white"
    app:layout_behavior=".behavior.BottomBehavior"/>
</android.support.design.widget.CoordinatorLayout>

好了坝橡,很簡(jiǎn)單的布局泻帮,需要注意的是,這個(gè)的 RecycleView 也設(shè)置了一個(gè) Behavior 哦计寇,這個(gè) Behavior 的使用我們?cè)谏弦黄┛鸵呀?jīng)講過(guò)了哦锣杂。然后給我們的“底部導(dǎo)航欄設(shè)置一個(gè) Behavior脂倦,使其在 RecycleView 向上滾動(dòng)的時(shí)候能夠隱藏起來(lái)。直接貼代碼吧

public class BottomBehavior extends CoordinatorLayout.Behavior {
public BottomBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

//依賴 AppBarLayout 的寫(xiě)法
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof AppBarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    int delta = dependency.getTop();
    child.setTranslationY(-delta);
    return true;
}

//-----------依賴 RecycleView 的寫(xiě)法-----------
//    @Override
//    public boolean layoutDependsOn(CoordinatorLayout parent, final View child, View dependency) {
//        boolean b = "Tag".equals(dependency.getTag());
//        if (b && this.child == null) {
//            this.child = child;
//            ((RecyclerView) dependency).addOnScrollListener(mOnScrollListener);
//        }
//        return b;
//    }
//
//    View child;
//    RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
//        @Override
//        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//            super.onScrolled(recyclerView, dx, dy);
//            float translationY = child.getTranslationY();
//            translationY += dy;
//            translationY = translationY > 0 ? (translationY > child.getHeight() ? child.getHeight() : translationY) : 0;
//            Log.e("translationY", translationY + "");
//            child.setTranslationY(translationY);
//        }
//    };

}

這里我用了兩種實(shí)現(xiàn)方式元莫,推薦使用第一種狼讨。因?yàn)榈诙N其實(shí)在 Activity 里面也是可以實(shí)現(xiàn)的。之所以講第二種寫(xiě)法是因?yàn)槠饩海覀冊(cè)趌ayoutDependsOn里面是可以獲取到 CoordinatorLayout 里面所有child 的引用,直接 CoordinatorLayout.findViewWithTag()即可播聪,然后我們可以為所欲為朽基,哈哈哈,看實(shí)際需求吧离陶。

Nested

Nested 機(jī)制要求 CoordinatorLayout 包含了一個(gè)實(shí)現(xiàn)了 NestedScrollingChild 接口的滾動(dòng)控件稼虎,如 RecycleView、NestedScrollView招刨、SwipeRefreshLayout等霎俩。然后我們 Behavior 里面的如下幾個(gè)方法會(huì)被回調(diào)哦~

onStartNestedScroll(View child, View target, int nestedScrollAxes)
onNestedPreScroll(View target, int dx, int dy, int[] consumed)
onNestedPreFling(View target, float velocityX, float velocityY)
onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
onStopNestedScroll(View target)

哦,對(duì)了沉眶,onStartNestedScroll 方法返回 ture表示要接受這個(gè)事件打却,后面的方法才會(huì)被調(diào)用哦。
看名字應(yīng)該都能看得懂這些方法在哪里會(huì)被回調(diào)吧谎倔,好了柳击,那我們來(lái)實(shí)現(xiàn)一個(gè)小 Demo 吧,看圖~

NestedDemo.gif

還是上面這個(gè) Demo片习,xml 布局里面添加一個(gè) FAB,給 FAB 設(shè)置一個(gè) Tag捌肴,然后CoordinatorLayout.findViewWithTag()找到 FAB 的引用,onStarNestedScroll 返回 true 表示要接受這個(gè)事件藕咏,onNestedPreScroll 里面根據(jù)滾動(dòng)的參數(shù) dy 判斷是上滑還是下拉状知,然后調(diào)用 FAb 的 hide 和 show 方法即可。

public class BottomBehavior extends CoordinatorLayout.Behavior {
public BottomBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

FloatingActionButton mFab;

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    if (mFab == null)
        mFab = (FloatingActionButton) parent.findViewWithTag("FAB");
    return dependency instanceof AppBarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    int delta = dependency.getTop();
    child.setTranslationY(-delta);
    return true;
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
    return true;
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    if (dy>10){
        mFab.hide();
    }else if (dy<-10){
        mFab.show();
    }
}
}

好了孽查,寫(xiě)完這幾個(gè) Demo饥悴,Behavior 的知識(shí)點(diǎn)算是基本上講完了,可能有些同學(xué)還是一頭霧水盲再。Wtah铺坞?這就講完了? 我還沒(méi)學(xué)會(huì)定制那些牛逼的 Behavior動(dòng)畫(huà)洲胖。
好吧济榨,其實(shí)我也是先學(xué)了簡(jiǎn)書(shū)、掘金上面好幾個(gè)熱門(mén)的 Behavior 應(yīng)用绿映,才開(kāi)始研究源碼的擒滑。說(shuō)實(shí)話腐晾,在寫(xiě)這篇文章之前,我真的不會(huì)用 Behavior丐一。

廢話說(shuō)得有點(diǎn)多藻糖,沒(méi)講重點(diǎn)。那我先說(shuō)重點(diǎn)吧库车,看懂了 Behavior 的原理再去看 Behavior 的 Demo 簡(jiǎn)直不要太簡(jiǎn)單巨柒。其實(shí)就是一些基本功底和一些屬性動(dòng)畫(huà)的集成,讓就成了高大上的 Behavior 動(dòng)畫(huà)柠衍。

來(lái)吧洋满,分析幾個(gè)別人的 Behavior Demo。
分析之前先鄭重聲明:1珍坊、我沒(méi)有獲得作者的授權(quán)牺勾,如果覺(jué)得我侵權(quán)了,請(qǐng)聯(lián)系我阵漏。2驻民、如果有評(píng)論措辭不當(dāng)?shù)牡胤竭€請(qǐng)多多包涵,僅代表個(gè)人觀點(diǎn)履怯,不針對(duì)任何人回还。3、還沒(méi)想好叹洲,以后想到再添加


Demo1

Demo1.gif

原文地址:傳送門(mén)

我的項(xiàng)目中也用到了這個(gè)效果懦趋,當(dāng)時(shí)我是基于 RecycleView 做了二次封裝實(shí)現(xiàn)的。蜜汁尷尬疹味,看到前兩天學(xué)習(xí) Behavior 的時(shí)候果斷重構(gòu)了代碼仅叫。

Demo 實(shí)現(xiàn)分析:就是一個(gè) AppBarLayout 使用了ScrollFlags 屬性實(shí)現(xiàn)了折疊效果,然后CollapsingToolbarLayout實(shí)現(xiàn)了圖片的視差滾動(dòng)糙捺。關(guān)于 AppBarLayout 的使用請(qǐng)看我上一篇文章诫咱。然后再加了一個(gè)下拉放大的效果,這個(gè)需要我們自己定制洪灯。
1.繼承AppBarLayout.Behavior坎缭,在任意包含 CoordinatorLayout 的方法里面獲取使用 CoordinatorLayout.findViewWithTag()方法獲取到需要下拉放大的 ImageView,記得設(shè)置 ImageView 的ClipChildren屬性為 false签钩。(不知道這個(gè)屬性功能的朋友自行找度娘或者 Google)
2.重新onNestedPreScroll掏呼,在這個(gè)方法里面去根據(jù)下拉距離Scale ImageView 即可。
3.手指松了之后還原铅檩,一個(gè)屬性動(dòng)畫(huà)解決
4.沒(méi)有4了憎夷,已經(jīng)實(shí)現(xiàn),具體代碼去看別人的源碼吧昧旨,真的很容易實(shí)現(xiàn)拾给,只是不知道的人不會(huì)而已祥得。


Demo2

Demo2.gif

哈哈,是不是很酷炫蒋得,反正當(dāng)時(shí)我看完之后感覺(jué)這一定是一個(gè)很酷炫的動(dòng)畫(huà)级及。
不用,其實(shí)兩個(gè) Behavior 就可以搞定了额衙。
先來(lái)分析一下有哪些效果吧饮焦。
1.下拉放大封面圖片
2.上拉頭像跟著個(gè)人信息欄目往上走
3.上拉頭像縮小
4.個(gè)人信息欄和工具欄視差滾動(dòng)
5.RecycleView 等其他欄目折疊完成再滾動(dòng)
6.TabLayout 固定在頂部
實(shí)現(xiàn):
1.參考上一個(gè) Demo
2.給頭像設(shè)置layout_anchor屬性懸掛在某個(gè)控件上即可
3.給頭像設(shè)置一個(gè) Behavior,使起 DependentAppBarLayout 窍侧,然后 Scale 即可县踢。
4.給兩個(gè) view 設(shè)置不同的layout_collapseParallaxMultiplier 系數(shù)即可,不知道這個(gè)屬性的回頭看我上一篇博客
5.給 RecycleView 設(shè)置AppBarLayout.ScrollingViewBehavior
6.可以放在 AppBarLayout 里面疏之,layoutflag 不設(shè)置 scroll 屬性即可。

具體實(shí)現(xiàn)請(qǐng)參考源碼:傳送門(mén)


Demo3

Demo3.gif

傳送門(mén)
哈哈暇咆,大家自己去看吧锋爪,這些效果的實(shí)現(xiàn)用到的知識(shí)點(diǎn)我的 MaterialDesign系列都講過(guò)的。
另外爸业,這幾個(gè) Demo 以及我文中的那幾個(gè) Demo其骄,大家記得都去敲一遍代碼,敲了一篇就會(huì)了扯旷,真的拯爽,不騙你,


有話說(shuō)

MaterialDesign 系列到這里就結(jié)束了钧忽,以后有時(shí)間再補(bǔ)充一下UI 篇吧毯炮。不過(guò)我這方面我也還是一知半懂的狀態(tài),等以后有心得了會(huì)動(dòng)筆的耸黑,這里我推薦扔物線的 HenCoder桃煎,我也在跟著學(xué),業(yè)界良心之作大刊,看完之后再也跟自定義 View 說(shuō) “No”为迈,產(chǎn)品再也不會(huì)說(shuō)“ 為什么 iOS 都實(shí)現(xiàn)了你們 Android 實(shí)現(xiàn)不了”。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末缺菌,一起剝皮案震驚了整個(gè)濱河市葫辐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伴郁,老刑警劉巖耿战,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異焊傅,居然都是意外死亡昆箕,警方通過(guò)查閱死者的電腦和手機(jī)鸦列,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鹏倘,“玉大人薯嗤,你說(shuō)我怎么就攤上這事∠吮茫” “怎么了骆姐?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)捏题。 經(jīng)常有香客問(wèn)我玻褪,道長(zhǎng),這世上最難降的妖魔是什么公荧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任带射,我火速辦了婚禮,結(jié)果婚禮上循狰,老公的妹妹穿的比我還像新娘窟社。我一直安慰自己,他們只是感情好绪钥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布灿里。 她就那樣靜靜地躺著,像睡著了一般程腹。 火紅的嫁衣襯著肌膚如雪匣吊。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天寸潦,我揣著相機(jī)與錄音色鸳,去河邊找鬼。 笑死见转,一個(gè)胖子當(dāng)著我的面吹牛缕碎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播池户,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼咏雌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了校焦?” 一聲冷哼從身側(cè)響起赊抖,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寨典,沒(méi)想到半個(gè)月后氛雪,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡耸成,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年报亩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浴鸿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弦追,死狀恐怖岳链,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情劲件,我是刑警寧澤掸哑,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站零远,受9級(jí)特大地震影響苗分,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牵辣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一摔癣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧纬向,春花似錦择浊、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)叉瘩。三九已至膳帕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間薇缅,已是汗流浹背危彩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泳桦,地道東北人汤徽。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像灸撰,于是被迫代替她去往敵國(guó)和親谒府。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容