Android8.0對(duì)于CoordinatorLayout、RecyclerView 精準(zhǔn)fling的優(yōu)化

之前為了開(kāi)發(fā)需求箕肃,學(xué)習(xí)了NestedScrolling機(jī)制婚脱,并使用CoordinatorLayout、AppBarLayout勺像、RecyclerView配合實(shí)現(xiàn)了相關(guān)的效果障贸,還寫了一篇關(guān)于分析原理的文章關(guān)于CoordinatorLayout AppBarLayout原理的一些分析,當(dāng)時(shí)做完需求以后,內(nèi)心其實(shí)是一只有種遺憾的吟宦,因?yàn)樵谑褂肦ecyclerView時(shí)篮洁,對(duì)于向上滑動(dòng)的fling效果其實(shí)是有問(wèn)題的,滑動(dòng)起來(lái)的感覺(jué)并不是連貫的殃姓,可以看一下這個(gè)效果:

old.gif

這個(gè)效果的體驗(yàn)其實(shí)并不是太好袁波,因?yàn)橄蛳耭ling時(shí)我們希望的效果是父布局可以在RecyclerView到達(dá)頂部時(shí),如果沒(méi)有消耗完fling時(shí)可以由父布局消耗從而繼續(xù)滑動(dòng)的蜗侈,可是由于之前的實(shí)現(xiàn)方式篷牌,并沒(méi)有辦法用常規(guī)的辦法達(dá)到這一點(diǎn),Google也意識(shí)到了這個(gè)問(wèn)題宛篇,所以在Android 8.0 擴(kuò)展了NestedScrolling的相關(guān)接口娃磺,使得滑動(dòng)可以變得更加順暢了,效果如下叫倍,簡(jiǎn)直如絲般順滑:


new.gif

在分析相關(guān)的原理之前偷卧,如果你們急需解決項(xiàng)目中的這種滑動(dòng)不順暢,可以先把解決方法告訴大家吆倦,那就是把compileSdkVersion升到26然后使用26.1.0的相關(guān)控件

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion '26.0.2'
    .......
}

dependencies {
 ....
    compile 'com.android.support:recyclerview-v7:26.1.0'
    compile 'com.android.support:support-v4:26+'
    compile 'com.android.support:design:26.1.0'
   .....
}

這樣改好之后听诸,不需要改動(dòng)一行代碼,即可完美實(shí)現(xiàn)如絲般順滑的fling蚕泽。

問(wèn)題發(fā)生的原因

RecyclerView中晌梨,fling的調(diào)用發(fā)生在滑動(dòng)事件MotionEvent.ACTION_UP時(shí),在25+版本之前RecycleView只是在fling的開(kāi)始之前通知了Parent是否消耗fling以及將fling分發(fā)到parent须妻,這只能做到Parent和RecyclerView同時(shí)fling或者Parent自己fling,在RecyclerView的fling過(guò)程中并沒(méi)有通知Parent仔蝌,在RecyclerView fling結(jié)束之后,Parent不能拿到剩余未消耗的距離荒吏,所以導(dǎo)致了這個(gè)不能連貫滑動(dòng)的問(wèn)題敛惊,25+版本的RecyclerView的fling方法如下:

public boolean fling(int velocityX, int velocityY) {
    if (mLayout == null) {
        Log.e(TAG, "Cannot fling without a LayoutManager set. " +
                "Call setLayoutManager with a non-null argument.");
        return false;
    }
    if (mLayoutFrozen) {
        return false;
    }

    final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
    final boolean canScrollVertical = mLayout.canScrollVertically();

    if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
        velocityX = 0;
    }
    if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
        velocityY = 0;
    }
    if (velocityX == 0 && velocityY == 0) {
        // If we don't have any velocity, return false
        return false;
    }

    if (!dispatchNestedPreFling(velocityX, velocityY)) {
        final boolean canScroll = canScrollHorizontal || canScrollVertical;
        dispatchNestedFling(velocityX, velocityY, canScroll);

        if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
            return true;
        }

        if (canScroll) {
            velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
            velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
            mViewFlinger.fling(velocityX, velocityY);
            return true;
        }
    }
    return false;
}

后面的代碼我就不貼了,給大家放一張時(shí)序圖绰更,表明RecyclerView的dispatchNestedPreFling和dispatchNestedFling是如何到達(dá)AppBarLayout的


時(shí)序圖1.png

可以看到瞧挤,一旦fling事件開(kāi)始之后锡宋,大家就各玩各的了。

網(wǎng)上的一些解決辦法

我在之前做需求時(shí)特恬,就對(duì)這個(gè)不能流暢滑動(dòng)感覺(jué)特別無(wú)奈执俩,本想著問(wèn)題找到了,那我就把一些類繼承一下重新實(shí)現(xiàn)一些方法唄癌刽,然后發(fā)現(xiàn)了基本上用到的類都是在design包下不對(duì)外開(kāi)放的役首,頓時(shí)發(fā)現(xiàn)后路被堵死,前幾天在簡(jiǎn)書(shū)上看到一個(gè)人實(shí)現(xiàn)的效果不錯(cuò)妒穴,他把所有相關(guān)的類都拷出來(lái)并且做了一些修改宋税,我感覺(jué)思路挺不錯(cuò)的摊崭,文章的地址是支付寶首頁(yè)交互三部曲 3 實(shí)現(xiàn)支付寶首頁(yè)交互,可以看看他文章里面的github上的代碼實(shí)現(xiàn)的效果讼油,很不錯(cuò)。
當(dāng)然我當(dāng)時(shí)并沒(méi)有像他這樣解決這個(gè)問(wèn)題呢簸,我在StackOverFlow看到了一個(gè)辦法矮台,這個(gè)方法可以保證RecyclerView在向下fling時(shí),如果第一個(gè)元素的位置不超過(guò)我們?cè)O(shè)定的閥值根时,那么我們就可以讓AppBarLayout和RecyclerView一起fling瘦赫,實(shí)現(xiàn)效果其實(shí)相對(duì)于不做處理會(huì)好很多,主要就是覆寫AppBarLayout.Behavior

public final class FlingBehavior extends AppBarLayout.Behavior {
    private static final int TOP_CHILD_FLING_THRESHOLD = 3;
    private boolean isPositive;

    public FlingBehavior() {
    }

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

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        //判斷是否上滑,并且第一個(gè)元素是否在閥值內(nèi)
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }
}

這樣寫好之后會(huì)比之前流暢很多

Android8.0的改變

Google也許也認(rèn)識(shí)到了這個(gè)問(wèn)題蛤迎,可能反饋這個(gè)問(wèn)題的人太多了吧确虱,終于在Android8.0的版本里解決掉了這個(gè)問(wèn)題,這個(gè)問(wèn)題讓他們來(lái)解決其實(shí)最合理替裆,因?yàn)楹芏囝惗际遣粚?duì)外開(kāi)放的校辩,開(kāi)發(fā)者如果自己解決肯定要把很多類自己考出來(lái)一份在修改一下,造成了不必要的浪費(fèi)辆童。
Google的解決辦法還是挺厲害的宜咒,他們并沒(méi)有頭疼治頭,而是把整個(gè)問(wèn)題抽象出來(lái)把鉴,升級(jí)了NestedScrolling相關(guān)的接口故黑,現(xiàn)在使用的都是NestedScrollingParent2和NestedScrollingChild2接口,用NestedScrollType來(lái)區(qū)分是touch觸發(fā)的滑動(dòng)還是非touch觸發(fā)的滑動(dòng)庭砍,例如NestedScrollingChild2繼承自NestedScrollingChild定義如下:

public interface NestedScrollingChild2 extends NestedScrollingChild {
     
    boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

    
    void stopNestedScroll(@NestedScrollType int type);

    
    boolean hasNestedScrollingParent(@NestedScrollType int type);

    
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type);

    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type);
}

可以發(fā)現(xiàn)CoordinatorLayout场晶、RecyclerView目前實(shí)現(xiàn)的接口不在是NestedScrollingParent和NestedScrollingChild,而是NestedScrollingParent2和NestedScrollingChild2怠缸。
解決這個(gè)問(wèn)題的主要辦法就是要在RecyclerView的整個(gè)fling過(guò)程中通知到Parent進(jìn)行一些處理诗轻,首先會(huì)在fling開(kāi)始之前通知Parent做記錄,然后在每次fling之前先通知Parent進(jìn)行消耗凯旭,在自己每次fling之后如果有剩余沒(méi)有消耗的距離概耻,在繼續(xù)傳遞給Parent使套,先看一下RecyclerView在改變前后的對(duì)比:


RecyclerView對(duì)比.png

然后ViewFlinger的執(zhí)行方法中也作出了改變,在每次fling滑動(dòng)之前會(huì)通知到Parent鞠柄,在滑動(dòng)結(jié)束之后會(huì)把剩余未消耗傳遞到Parent侦高,也是解決這個(gè)問(wèn)題的關(guān)鍵,對(duì)比如下:


ViewFlinger對(duì)比.png
ViewFlinger對(duì)比2.png

整個(gè)過(guò)程的分析

上面主要對(duì)8.0版本和之前版本的實(shí)現(xiàn)差別做了對(duì)比厌杜,現(xiàn)在通過(guò)對(duì)整個(gè)流程的分析奉呛,來(lái)解釋Google時(shí)如何解決這個(gè)問(wèn)題的,接著上面RecyclerView在fling開(kāi)始的時(shí)候調(diào)用startNestedScroll方法開(kāi)始夯尽,我們可以根據(jù)源碼畫出如下的流程圖:


fling過(guò)程.png

這里涉及到的整個(gè)源碼有點(diǎn)略多瞧壮,所以大家可以根據(jù)流程圖的過(guò)程去查看相應(yīng)的源碼,這里主要把這些過(guò)程分成幾個(gè)組匙握,然后只要看最關(guān)鍵的代碼就可以了咆槽。

  • 第一組:流程圖中的1-9過(guò)程
    這個(gè)過(guò)程主要是在RecyclerView fling之前,記錄一下ParentView圈纺,這個(gè)ParentView的type是TYPE_NON_TOUCH的秦忿,這樣在接下來(lái)的處理過(guò)程當(dāng)中,我們所有相關(guān)的ParentView都是在這里記錄下來(lái)的View蛾娶,如果此時(shí)ParentView不做處理灯谣,那么后續(xù)的操作都不回傳遞到ParentView當(dāng)中,這個(gè)之前的NestedScrolling的方法的意思是一樣的蛔琅,只不過(guò)這里多了一個(gè)type胎许。NestedScrollingChildHelper對(duì)應(yīng)源碼如下:
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    if (hasNestedScrollingParent(type)) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}
  • 第二組:流程圖中的10-13過(guò)程
    這里的思想就是NestScrolling的核心思想,在我未消耗之前罗售,先傳遞給ParentView做消耗辜窑,我消耗剩余的部分,RecyclerView.ViewFlinger相關(guān)源碼如下:
public void run() {
    if (mLayout == null) {
        stop();
        return; // no layout, cannot scroll.
    }
    disableRunOnAnimationRequests();
    consumePendingUpdateOperations();
    // keep a local reference so that if it is changed during onAnimation method, it won't
    // cause unexpected behaviors
    final OverScroller scroller = mScroller;
    final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
    if (scroller.computeScrollOffset()) {
        final int[] scrollConsumed = mScrollConsumed;
        final int x = scroller.getCurrX();
        final int y = scroller.getCurrY();
        int dx = x - mLastFlingX;
        int dy = y - mLastFlingY;
        int hresult = 0;
        int vresult = 0;
        mLastFlingX = x;
        mLastFlingY = y;
        int overscrollX = 0, overscrollY = 0;
        //在這里傳遞
        if (dispatchNestedPreScroll(dx, dy, scrollConsumed, null, TYPE_NON_TOUCH)) {
            dx -= scrollConsumed[0];
            dy -= scrollConsumed[1];
        }
        ..................
    }
}

NestedScrollingChildHelper的dispatchNestedPreScroll如下:

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
        @Nullable int[] offsetInWindow, @NestedScrollType int type) {
    if (isNestedScrollingEnabled()) {
        //如果對(duì)應(yīng)的type沒(méi)有記錄下來(lái)莽囤,那么不做處理直接返回false
        final ViewParent parent = getNestedScrollingParentForType(type);
        if (parent == null) {
            return false;
        }

        if (dx != 0 || dy != 0) {
            int startX = 0;
            int startY = 0;
            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }

            if (consumed == null) {
                if (mTempNestedScrollConsumed == null) {
                    mTempNestedScrollConsumed = new int[2];
                }
                consumed = mTempNestedScrollConsumed;
            }
            consumed[0] = 0;
            consumed[1] = 0;
            ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

            if (offsetInWindow != null) {
                mView.getLocationInWindow(offsetInWindow);
                offsetInWindow[0] -= startX;
                offsetInWindow[1] -= startY;
            }
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}
  • 第三組: 流程圖中的14-18過(guò)程
    這個(gè)過(guò)程谬擦,就是解決之前不能夠準(zhǔn)確fling的關(guān)鍵步驟,因?yàn)橹暗腣iewFlinger對(duì)于沒(méi)有消耗完的距離只是判斷了除了自己消耗完以外朽缎,剩下的不是0那么就會(huì)做一些清除動(dòng)畫之類的操作惨远,所以并沒(méi)有給ParentView一個(gè)繼續(xù)滑動(dòng)的機(jī)會(huì),這次除了判斷不等于0的條件外话肖,還要把剩余的距離傳遞給ParentView北秽,給ParentView一個(gè)準(zhǔn)確Fling的機(jī)會(huì),ViewFlinger的dispatchNestedScroll方法會(huì)調(diào)用到NestedScrollingChildHelper的dispatchNestedScroll方法最筒,然后根據(jù)流程圖的順序贺氓,一直調(diào)用到AppBarLayout的onNestedScroll方法,我們可以看一下AppBarLayout的onNestedScroll就會(huì)覺(jué)得恍然大悟:
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
        View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
        int type) {
    if (dyUnconsumed < 0) {
        // If the scrolling view is scrolling down but not consuming, it's probably be at
        // the top of it's content
        scroll(coordinatorLayout, child, dyUnconsumed,
                -child.getDownNestedScrollRange(), 0);
    }
}

這里只做了一件事兒床蜘,翻譯一下注釋的意思就是辙培,“dyUnconsumed<0的時(shí)候蔑水,說(shuō)明View的內(nèi)容正在向下滑動(dòng),并且沒(méi)有消耗完滑動(dòng)事件扬蕊,可能是View已經(jīng)到達(dá)了內(nèi)容的頂部”搀别,所以在出現(xiàn)了這種情況的時(shí)候,AppBarLayout回調(diào)用自己的scroll方法來(lái)繼續(xù)消耗尾抑,從而達(dá)到了精準(zhǔn)fling的目的歇父。

整個(gè)流程就分析完了,看了這塊的源碼再愈,感覺(jué)對(duì)這部分的內(nèi)容理解又加深了一下榜苫,希望以后能有時(shí)間在多看看其他的源碼,看源碼的收獲還是挺大的~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翎冲,一起剝皮案震驚了整個(gè)濱河市垂睬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌府适,老刑警劉巖羔飞,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異檐春,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)么伯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門疟暖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人田柔,你說(shuō)我怎么就攤上這事俐巴。” “怎么了硬爆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵欣舵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我缀磕,道長(zhǎng)缘圈,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任袜蚕,我火速辦了婚禮糟把,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘牲剃。我一直安慰自己遣疯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布凿傅。 她就那樣靜靜地躺著缠犀,像睡著了一般数苫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辨液,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天文判,我揣著相機(jī)與錄音,去河邊找鬼室梅。 笑死戏仓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的亡鼠。 我是一名探鬼主播赏殃,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼间涵!你這毒婦竟也來(lái)了仁热?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤勾哩,失蹤者是張志新(化名)和其女友劉穎抗蠢,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體思劳,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡迅矛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了潜叛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秽褒。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖威兜,靈堂內(nèi)的尸體忽然破棺而出销斟,到底是詐尸還是另有隱情,我是刑警寧澤椒舵,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布蚂踊,位于F島的核電站,受9級(jí)特大地震影響笔宿,放射性物質(zhì)發(fā)生泄漏犁钟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一措伐、第九天 我趴在偏房一處隱蔽的房頂上張望特纤。 院中可真熱鬧,春花似錦侥加、人聲如沸捧存。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)昔穴。三九已至镰官,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吗货,已是汗流浹背泳唠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宙搬,地道東北人笨腥。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像勇垛,于是被迫代替她去往敵國(guó)和親脖母。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,528評(píng)論 25 707
  • 這幾天學(xué)了一些CoordinatorLayout闲孤、AppBarLayout配合使用的一些方法谆级,之前還寫了一篇Coo...
    HumorousMan閱讀 3,973評(píng)論 4 22
  • 簡(jiǎn)介: 提供一個(gè)讓有限的窗口變成一個(gè)大數(shù)據(jù)集的靈活視圖。 術(shù)語(yǔ)表: Adapter:RecyclerView的子類...
    酷泡泡閱讀 5,140評(píng)論 0 16
  • 如果沒(méi)有你 時(shí)間怎么過(guò) 如果沒(méi)有你 ...
    小眼爾姑涼閱讀 297評(píng)論 0 0
  • 想來(lái)想去讼积,實(shí)在想不出怎么結(jié)合后五課的內(nèi)容做肥照,然后就做了下面這種。勤众。舆绎。 字體拉長(zhǎng)的那兩條直線式用插入形狀的直線來(lái)畫出...
    酗酒的貓閱讀 238評(píng)論 1 1