MagicaSakura解析

MagicaSakura

bilibili的又一Android開源作品, 可以無閃屏地對(duì)程序中的控件更換主題色, 其采用的遍歷View樹的方式對(duì)每一個(gè)控件進(jìn)行操作(區(qū)別于保存集合). 在控件變色上使用的是對(duì)Drawable進(jìn)行tint(區(qū)別于只對(duì)Drawable或者ImageView設(shè)置ColorFilter), 其中使用到了V4包的DrawableCompat, 還對(duì)特別的View進(jìn)行了特殊處理. 使用TintDrawable的方式不會(huì)影響原來的屬性和使用方式. 要說明的是這種方式要對(duì)所有要變色的View進(jìn)行自定義, 以后項(xiàng)目中就不能夠好好寫換件了...更多的介紹可以看原作者的介紹.

MagicaSakura使用

原作者有在博客中說明使用方法: 實(shí)現(xiàn)切換顏色的SwitchColor, 重寫其兩個(gè)方法. 再有要自己確定各個(gè)主題色, 然后切換主題色時(shí)使用的方法是ThemeUtils的一個(gè)全局方法refreshUI, 它最終會(huì)使用到SwitchColor來得到色值.

MagicaSakura分析

下面先分析換扶主要流程, 再去分析每一個(gè)View進(jìn)行換膚的流程, 最后再說一些特殊的View進(jìn)行換膚的細(xì)節(jié)

流程分析

首先要去自己實(shí)現(xiàn)SwitchColor, 并通過ThemeUtils將其注冊(cè)成為全局變量, 在以后的換膚中方便使用.

//將切換顏色的對(duì)象作為全局變量存儲(chǔ)起來
ThemeUtils.setSwitchColor(this);

其中的this實(shí)現(xiàn)了SwitchColor接口, 負(fù)責(zé)給出皮膚的顏色, 通過兩個(gè)接口方法給出.

public interface switchColor {
    //通過指定ID來更換顏色
    @ColorInt int replaceColorById(Context context, @ColorRes int colorId);

    @ColorInt int replaceColor(Context context, @ColorInt int color);
}

下面分析在我們點(diǎn)換膚的時(shí)候程序運(yùn)程的流程
上面說過每一次換膚都要對(duì)View樹進(jìn)行遍歷, 封閉遍歷的方法在ThemeUtils.refreshUI(Context context, ExtraRefreshable extraRefreshable)中.

//在這里對(duì)整個(gè)view樹進(jìn)行遍歷
public static void refreshUI(Context context, ExtraRefreshable extraRefreshable) {
    TintManager.clearTintCache();
    Activity activity = getWrapperActivity(context);
    if (activity != null) {
        if (extraRefreshable != null) {
            extraRefreshable.refreshGlobal(activity);
        }
        //對(duì)contentView進(jìn)行遍歷
        View rootView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
        refreshView(rootView, extraRefreshable);
    }
}

兩個(gè)參數(shù), ctx不用說, 第二個(gè)ExtraRefreshable接口有兩個(gè)方法, void refreshGlobal(Activity activity);是每次換膚是調(diào)用一次的方法, void refreshSpecificView(View view)是對(duì)特殊的View進(jìn)行染色時(shí)都要調(diào)用的方法.
我們可以看到他是通過對(duì)Activity去拿到contentView去進(jìn)行遍歷的. refreshView(rootView, extraRefreshable);是對(duì)View樹進(jìn)行遞歸遍歷的方法.

private static void refreshView(View view, ExtraRefreshable extraRefreshable) {
    if (view == null) return;
    //下面進(jìn)行遞歸遍歷
    view.destroyDrawingCache();
    if (view instanceof Tintable) {
        //最關(guān)鍵的部分, 拿到每個(gè)view后tint一下
        ((Tintable) view).tint();
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
                refreshView(((ViewGroup) view).getChildAt(i), extraRefreshable);
            }
        }
    } else {
        if (extraRefreshable != null) {
            extraRefreshable.refreshSpecificView(view);
        }
        //ListView和GridView之類
        if (view instanceof AbsListView) {
            ListAdapter adapter = ((AbsListView) view).getAdapter();
            //拿到根本的Adapter
            while (adapter instanceof WrapperListAdapter) {
                adapter = ((WrapperListAdapter) adapter).getWrappedAdapter();
            }
            if (adapter instanceof BaseAdapter) {
                ((BaseAdapter) adapter).notifyDataSetChanged();
            }
        }
        if (view instanceof RecyclerView) {
            try {
                if (mRecycler == null) {
                    mRecycler = RecyclerView.class.getDeclaredField("mRecycler");
                    mRecycler.setAccessible(true);
                }
                if (mClearMethod == null) {
                    mClearMethod = Class.forName("android.support.v7.widget.RecyclerView$Recycler")
                            .getDeclaredMethod("clear");
                    mClearMethod.setAccessible(true);
                }
                mClearMethod.invoke(mRecycler.get(view));
            } catch (NoSuchMethodException e) {
                ...
            }
            ((RecyclerView) view).getRecycledViewPool().clear();
            ((RecyclerView) view).invalidateItemDecorations();
        }
        //不是tintabale, 遍歷孩子
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
                refreshView(((ViewGroup) view).getChildAt(i), extraRefreshable);
            }
        }
    }
}

整個(gè)過程就是遞歸遍歷, Tintable的實(shí)現(xiàn)就直接渲染, 是ViewGroup就遞歸, 是ListView(GridView)或者RecylerView就notify一下, 就這么完了, 這就是一個(gè)簡(jiǎn)單的流程了.
其中的tint方法就是對(duì)每個(gè)具體的View進(jìn)行渲染, 達(dá)到自定義顏色的效果. 具體tint的過程下面會(huì)講一下, 幾乎對(duì)所有常用的View進(jìn)行了重寫, 工作量很大, 但一個(gè)控件的流程走通了, 其他的控件原理都是類似的, 也很快就了解了. 此時(shí)配上MagicaSakura的包結(jié)果圖, 可以看到widgets包下的所有常用View都被重寫了.

MagicaSakura的包結(jié)構(gòu)

View進(jìn)行渲染過程

對(duì)View的渲染都是通過在View中保存的幾個(gè)Helper實(shí)現(xiàn)的, 每個(gè)要換膚的View在構(gòu)造的時(shí)候會(huì)根據(jù)跟隨皮膚變化的屬性構(gòu)建對(duì)應(yīng)的Helper, 比如說TextView在換膚的時(shí)候要變換自己的TextColor, BackGround以及drawableLeft, drawableRight之類的屬性所以在TintTextView中會(huì)保存對(duì)應(yīng)的三個(gè)Helper, 如圖:

TintTextView中的Helper

這么做不僅能將換膚功能的代碼解耦出來, 最重要的是可以在不同的控件上復(fù)用這個(gè)Helper, 比如TintImageView也要在換膚時(shí)對(duì)Background進(jìn)行變換, 直接重用AppCompatBackgroundHelper就可以了.
下面以TextView為例, 分析一下作者是怎樣讓一個(gè)View能顯示任意一種顏色, 并且還能動(dòng)態(tài)地切換View的色值.
先看其構(gòu)造方法:

public TintTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    if (isInEditMode()) {
        return;
    }
    //TintManager負(fù)責(zé)管理Drawable資源, 后面會(huì)講到
    TintManager tintManager = TintManager.get(getContext());
    //控制TextColor之類的屬性
    mTextHelper = new AppCompatTextHelper(this, tintManager);
    mTextHelper.loadFromAttribute(attrs, defStyleAttr);
    //控制Background屬性
    mBackgroundHelper = new AppCompatBackgroundHelper(this, tintManager);
    mBackgroundHelper.loadFromAttribute(attrs, defStyleAttr);
    //控制DrawableLeft, DrawableRight之類的屬性
    mCompoundDrawableHelper = new AppCompatCompoundDrawableHelper(this, tintManager);
    mCompoundDrawableHelper.loadFromAttribute(attrs, defStyleAttr);
}

TintTextView使用了三個(gè)Helper,都作為成員保存起來, 構(gòu)造出來之后直接調(diào)用其void loadFromAttribute(AttributeSet attrs, int defStyleAttr)方法, 其它Helper也是類似. 這三個(gè)Helper就在View的tint方法中類似于下面方式使用. 另外, 涉及這些屬性變化的方法都要進(jìn)行重寫, 都要使用這些Helper進(jìn)行變化屬性值.

if (mTextHelper != null) {
    mTextHelper.tint();
}

下面以AppCompatTextHelper為例分析他們的工作原理.
其構(gòu)造方法只是將當(dāng)前的viewtintManager保存為成員. 其loadFromAttribute方法要對(duì)View的幾個(gè)屬性進(jìn)行處理, 代碼如下:

void loadFromAttribute(AttributeSet attrs, int defStyleAttr) {
    TypedArray array = mView.getContext().obtainStyledAttributes(attrs, ATTRS, defStyleAttr, 0);

    int textColorId = array.getResourceId(0, 0);
    if (textColorId == 0) {//如果沒有設(shè)定TextColor就使用TextAppearance
        setTextAppearanceForTextColor(array.getResourceId(2, 0), false);
    } else {
        setTextColor(textColorId);//此方法去會(huì)去找到真正的顏色并且設(shè)置給這個(gè)View
    }

    if (array.hasValue(1)) {
        setLinkTextColor(array.getResourceId(1, 0));
    }
    array.recycle();
}

插一句, 當(dāng)時(shí)看這一點(diǎn)的時(shí)候犯了個(gè)迷糊...這里使用了0, 2什么的是是因?yàn)樯厦鎸?duì)ATTRS的定義:

private static final int[] ATTRS = {
        android.R.attr.textColor,
        android.R.attr.textColorLink,
        android.R.attr.textAppearance,
};
//這里就要處理三個(gè)屬性, 所在先組成一個(gè)數(shù)組
//拿到的TypeArray里面就應(yīng)該只有三個(gè)值, 這也是后面使用0, 1, 2的原因

回到正題上來, 看setTextColor方法

private void setTextColor(@ColorRes int resId) {
    if (mTextColorId != resId) {
        //記錄色值, 清除染色信息, 放心, 在下面一句又將這個(gè)信息給加上了
        resetTextColorTintResource(resId);
        if (resId != 0) {
            setSupportTextColorTint(resId);
        }
    }
}
//設(shè)置染色信息
private void setSupportTextColorTint(int resId) {
    if (resId != 0) {
        if (mTextColorTintInfo == null) {
            mTextColorTintInfo = new TintInfo();
        }
        mTextColorTintInfo.mHasTintList = true;
        //這個(gè)過程會(huì)在后面解釋, 就是能拿到要渲染的ColorStateList
        mTextColorTintInfo.mTintList =  mTintManager.getColorStateList(resId);
    }
    applySupportTextColorTint();
}

applySupportTextColorTint中直接使用了上面的mTextColorTintInfo.mTintList, 直接將其設(shè)置給TextView. Helper也會(huì)有tint方法, 此方法會(huì)對(duì)View進(jìn)行渲染, 類似于

if (mTextColorId != 0) {
    setSupportTextColorTint(mTextColorId);
}

TintManager分析

還剩下最后一部分, TintManager是怎么找到Drawable并給他設(shè)置了皮膚包的顏色的, 下面進(jìn)行簡(jiǎn)單分析

@Nullable
public ColorStateList getColorStateList(@ColorRes int resId) {
    if (resId == 0) return null;
    //對(duì)Ctx進(jìn)行弱引用處理
    final Context context = mContextRef.get();
    if (context == null) return null;
    //對(duì)colorStateList進(jìn)行了LRU緩存處理
    ColorStateList colorStateList = mCacheTintList != null ? mCacheTintList.get(resId) : null;
    if (colorStateList == null) {
        colorStateList = ColorStateListUtils.createColorStateList(context, resId);//創(chuàng)建tintcolorStateList
        if (colorStateList != null) {
            if (mCacheTintList == null) {
                mCacheTintList = new SparseArray<>();
            }
            mCacheTintList.append(resId, colorStateList);
        }
    }
    return colorStateList;
}

這段代碼主要是處理異常和緩存問題, 真正拿到ColorStateList是在ColorStateListUtils.createColorStateList(context, resId);的方法中.

static ColorStateList createColorStateList(Context context, int resId) {
    if (resId <= 0) return null;

    TypedValue value = new TypedValue();
    context.getResources().getValue(resId, value, true);
    ColorStateList cl = null;
    if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
            && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
        cl = ColorStateList.valueOf(ThemeUtils.replaceColorById(context, value.resourceId));
    } else {
        final String file = value.string.toString();
        try {
            if (file.endsWith("xml")) {
                final XmlResourceParser rp = context.getResources().getAssets().openXmlResourceParser(
                        value.assetCookie, file);
                final AttributeSet attrs = Xml.asAttributeSet(rp);
                int type;

                while ((type = rp.next()) != XmlPullParser.START_TAG
                        && type != XmlPullParser.END_DOCUMENT) {
                    // Seek parser to start tag.
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new XmlPullParserException("No start tag found");
                }

                cl = createFromXmlInner(context, rp, attrs);
                rp.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (XmlPullParserException e) {
            e.printStackTrace();
        }
    }
    return cl;
}

木有錯(cuò), 作者選擇了直接去解析XML, 這是我當(dāng)時(shí)做換膚時(shí)根本不考慮的方式, 想不到就這樣被實(shí)現(xiàn)了...其中使用了Android對(duì)XML文件進(jìn)行解析的方法, 非常值得我們?nèi)W(xué)習(xí). 通過資源的ID去取資源的信息, 如果只是顏色值就創(chuàng)建ColorStateList, 再如果資源是XML文件的話就開始解析這個(gè)文件.
createFromXmlInner中判斷了文件是不是一個(gè)selector, 是的話才繼續(xù)執(zhí)行, 否則不處理. 繼續(xù)執(zhí)行會(huì)調(diào)用到static ColorStateList inflateColorStateList(Context context, XmlPullParser parser, AttributeSet attrs) throws IOException, XmlPullParserException的方法, 來看看真正的實(shí)現(xiàn)

static ColorStateList inflateColorStateList(Context context, XmlPullParser parser, AttributeSet attrs) throws IOException, XmlPullParserException {
    final int innerDepth = parser.getDepth() + 1;
    int depth;
    int type;
    LinkedList<int[]> stateList = new LinkedList<>();
    LinkedList<Integer> colorList = new LinkedList<>();

    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
            && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
        if (type != XmlPullParser.START_TAG || depth > innerDepth
                || !parser.getName().equals("item")) {
            continue;
        }
        TypedArray a1 = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.color});
        //這里面會(huì)使用到最開始的SwitchColor, 拿到真正的Color
        final int baseColor = com.bilibili.magicasakura.utils.ThemeUtils.replaceColorById(context, a1.getResourceId(0, Color.MAGENTA));
        a1.recycle();
        TypedArray a2 = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.alpha});
        final float alphaMod = a2.getFloat(0, 1.0f);
        a2.recycle();
        colorList.add(alphaMod != 1.0f
                ? ColorUtils.setAlphaComponent(baseColor, Math.round(Color.alpha(baseColor) * alphaMod))
                : baseColor);

        stateList.add(extractStateSet(attrs));
    }

    if (stateList.size() > 0 && stateList.size() == colorList.size()) {
        int[] colors = new int[colorList.size()];
        for (int i = 0; i < colorList.size(); i++) {
            colors[i] = colorList.get(i);
        }
        return new ColorStateList(stateList.toArray(new int[stateList.size()][]), colors);
    }
    return null;
}

上面的所有就是對(duì)換膚的流程進(jìn)行了一個(gè)簡(jiǎn)單的分析, 是否能在自己的項(xiàng)目中使用這個(gè)庫已經(jīng)可以做出部分判斷, 還有很多的細(xì)節(jié)沒有講到, 下面會(huì)無規(guī)則地介紹一些細(xì)節(jié)的問題.

部分細(xì)節(jié)問題

  • TintMManager中使用了LruCache, 對(duì)于解析等到的Drawable要進(jìn)行緩存, 下次再取用的時(shí)候可以不去解析XML這么復(fù)雜
  • 解析資源時(shí)主要支持下面三種Drawable, 對(duì)于不同的Drawable解析的方式也不全一樣
    支持的Drawable類型
  • 程序中使用的ColorFilter都是PorterDuffColorFilter
  • 上面的例子中使用的是AppCompatTextHelper, 還有另一種使用更多的方式渲染(比如在AppCompatBackgroundHelper中)
private boolean applySupportBackgroundTint() {
    Drawable backgroundDrawable = mView.getBackground();
    if (backgroundDrawable != null && mBackgroundTintInfo != null && mBackgroundTintInfo.mHasTintList) {
        backgroundDrawable = DrawableCompat.wrap(backgroundDrawable);
        backgroundDrawable = backgroundDrawable.mutate();
        if (mBackgroundTintInfo.mHasTintList) {
            DrawableCompat.setTintList(backgroundDrawable, mBackgroundTintInfo.mTintList);
        }
        if (mBackgroundTintInfo.mHasTintMode) {
            DrawableCompat.setTintMode(backgroundDrawable, mBackgroundTintInfo.mTintMode);
        }
        if (backgroundDrawable.isStateful()) {
            backgroundDrawable.setState(mView.getDrawableState());
        }
        setBackgroundDrawable(backgroundDrawable);
        return true;
    }
    return false;
}

其中使用了V4包的DrawableCompat, 才能使用setTintList.

  • 如果DrawableColorDrawable的話是不能設(shè)置ColorFilter的, 在API21以下都是不起效果的, 使用的是ColorDrawablesetColor方法.

結(jié)語

MagicaSakura中將解析XML, 屬性渲染, 控件功能分離得很好, 結(jié)構(gòu)非常清晰利于擴(kuò)展, 可以學(xué)到很多.
他使用系統(tǒng)解析XML方法去自己解析, 值得學(xué)習(xí).
如果項(xiàng)目中要實(shí)現(xiàn)換膚功能的話可以考慮使用, 就是項(xiàng)目如果已經(jīng)比較大的話, 工作量可能也會(huì)很大, 也可以考慮一下Android_Skin_Loader也是不錯(cuò)的選擇.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子识补,更是在濱河造成了極大的恐慌阅签,老刑警劉巖兼搏,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衙傀,死亡現(xiàn)場(chǎng)離奇詭異盈蛮,居然都是意外死亡兄纺,警方通過查閱死者的電腦和手機(jī)大溜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來估脆,“玉大人钦奋,你說我怎么就攤上這事「碓” “怎么了付材?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)圃阳。 經(jīng)常有香客問我厌衔,道長(zhǎng),這世上最難降的妖魔是什么捍岳? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任富寿,我火速辦了婚禮,結(jié)果婚禮上锣夹,老公的妹妹穿的比我還像新娘页徐。我一直安慰自己,他們只是感情好银萍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布变勇。 她就那樣靜靜地躺著,像睡著了一般贴唇。 火紅的嫁衣襯著肌膚如雪搀绣。 梳的紋絲不亂的頭發(fā)上飞袋,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音链患,去河邊找鬼巧鸭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛锣险,可吹牛的內(nèi)容都是我干的蹄皱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼芯肤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了压鉴?” 一聲冷哼從身側(cè)響起崖咨,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎油吭,沒想到半個(gè)月后击蹲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡婉宰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年歌豺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片心包。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡类咧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蟹腾,到底是詐尸還是另有隱情痕惋,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布娃殖,位于F島的核電站值戳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏炉爆。R本人自食惡果不足惜堕虹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芬首。 院中可真熱鬧赴捞,春花似錦、人聲如沸衩辟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艺晴。三九已至昼钻,卻和暖如春掸屡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背然评。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工仅财, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碗淌。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓盏求,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親亿眠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碎罚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,099評(píng)論 25 707
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,757評(píng)論 22 665
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件纳像、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,101評(píng)論 4 62
  • 女人是個(gè)運(yùn)動(dòng)愛好者荆烈,這天在跑完10公里后,突然感覺右腿大腿根部非常疼痛竟趾,特別走路的時(shí)候憔购,疼痛劇烈。于是岔帽,決定去醫(yī)院...
    艾娃閱讀 397評(píng)論 0 0
  • 我知道我喜歡你玫鸟,就像一場(chǎng)莫名其妙的感冒,沒有緣由的犀勒,就已經(jīng)陷入了頭疼屎飘、咳嗽、發(fā)熱的表象里账蓉。我知道你不愛我啊枚碗,在從每...
    西顧AVIVI閱讀 821評(píng)論 2 7