Android動態(tài)換膚原理解析及實(shí)踐

前言:

本文主要講述如何在項(xiàng)目中,在不重啟應(yīng)用的情況下甩十,實(shí)現(xiàn)動態(tài)換膚的效果船庇。換膚這塊做的比較好的,有網(wǎng)易云音樂侣监,qq等鸭轮,給用戶帶來了多樣的界面選擇和個(gè)性化定制。之前看到換膚的效果后對這塊也比較好奇橄霉,就抽時(shí)間研究了下窃爷,今天給大家分享解析原理和實(shí)踐中遇到的問題。

為什么要做動態(tài)換膚:

  • 動態(tài)換膚可以滿足日常產(chǎn)品和運(yùn)營需求姓蜂,滿足用戶個(gè)性化界面定制的需求等等按厘。
  • 動態(tài)換膚,相比于靜態(tài)皮膚覆糟,可以減小apk大小
  • 皮膚模塊獨(dú)立便于維護(hù)
  • 由服務(wù)器下發(fā)刻剥,不需要發(fā)版即可實(shí)現(xiàn)動態(tài)更新

換膚的一般實(shí)現(xiàn)思路:

  • 資源打包靜態(tài)替換方案:
    指定資源路徑地址,在打包時(shí)將對應(yīng)資源打包進(jìn)去
    build.gradle中進(jìn)行對應(yīng)配置

    sourceSets {
    // 測試版本和線上版本用同一套資源
      YymTest {
          res.srcDirs = ["src/Yym/res", "src/YymTest/res"]
          assets.srcDirs = ["src/Yym/assets"]
       }
     }
    

    這種方式是在打包時(shí)滩字,通過指定資源文件的路徑在編譯打包時(shí)將對應(yīng)的資源打包進(jìn)去造虏,以實(shí)現(xiàn)不同的主題樣式等換膚需求。適合發(fā)布馬甲版本的app需求麦箍。

  • 動態(tài)換膚方案:
    應(yīng)用運(yùn)行時(shí)漓藕,選擇皮膚后,在主app中拿到對應(yīng)皮膚包的Resource挟裂,將皮膚包中的
    資源動態(tài)加載到應(yīng)用中展示并呈現(xiàn)給用戶享钞。

動態(tài)換膚的一般步驟為:

  1. 下載并加載皮膚包
  2. 拿到皮膚包Resource對象
  3. 標(biāo)記需要換膚的View
  4. 切換時(shí)即時(shí)刷新頁面
  5. 制作皮膚包
  6. 換膚整體框架的搭建

如何拿到皮膚包Resouce對象:

PackageManager mPm = context.getPackageManager();
                    PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, 
PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;

AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);

Resources superRes = context.getResources();
Resources skinResource = new 
Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());

其中需要傳入的參數(shù)即為皮膚包的文件路徑地址,還有當(dāng)前app的context
其中superResource為當(dāng)前app的Resource對象诀蓉,而skinResource即為加載后的皮膚包的Resource對象栗竖。
皮膚包的資源即可通過skinResource.getIdentifier(resName, "color", skinPackageName);這種方式拿到了暑脆。

如何標(biāo)記需要換膚的View

  • 如何找到需要換膚的View

    1)通過xml標(biāo)記的View:
    這種方式主要要通過實(shí)現(xiàn)LayoutInflate.Factory2這個(gè)接口(為支持AppcompotActivty 用LayoutInflaterFactory API是一樣的)。

/**
* Used with {@code LayoutInflaterCompat.setFactory()}. Offers the same API as
* {@code LayoutInflater.Factory2}.
*/
public interface LayoutInflaterFactory {

/**
 * Hook you can supply that is called when inflating from a LayoutInflater.
 * You can use this to customize the tag names available in your XML
 * layout files.
 *
 * @param parent The parent that the created view will be placed
 * in; <em>note that this may be null</em>.
 * @param name Tag name to be inflated.
 * @param context The context the view is being created in.
 * @param attrs Inflation attributes as specified in XML file.
 *
 * @return View Newly created view. Return null for the default
 *         behavior.
 */
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

LayoutInflater 提供了setFactory(LayoutInflater.Factory factory)和setFactory2(LayoutInflater.Factory2 factory)兩個(gè)方法可以讓你去自定義布局的填充(有點(diǎn)類似于過濾器狐肢,我們在填充這個(gè)View之前可以做一些額外的事)添吗,F(xiàn)actory2 是在API 11才添加的。
通過實(shí)現(xiàn)這兩個(gè)接口可以實(shí)現(xiàn)View的重寫份名。Activity本身就默認(rèn)實(shí)現(xiàn)了Factory接口碟联,所以我們復(fù)寫了Factory的onCreateView之后,就可以不通過系統(tǒng)層而是自己截獲從xml映射的View進(jìn)行相關(guān)View創(chuàng)建的操作僵腺,包括對View的屬性進(jìn)行設(shè)置(比如背景色鲤孵,字體大小,顏色等)以實(shí)現(xiàn)換膚的效果辰如。如果onCreateView返回null的話普监,會將創(chuàng)建View的操作交給Activity默認(rèn)實(shí)現(xiàn)的Factory的onCreateView處理。

SkinInflaterFactory:

public class SkinInflaterFactory implements LayoutInflaterFactory {

private static final boolean DEBUG = true;

/**
 * Store the view item that need skin changing in the activity
 */
private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    // if this is NOT enable to be skined , simplly skip it
    boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
    Log.d("ansen", "isSkinEnable----->" + isSkinEnable);
    Log.d("ansen", "name----->" + name);
    if (!isSkinEnable) {
        return null;
    }

    View view = createView(context, name, attrs);

    if (view == null) {
        return null;
    }

    parseSkinAttr(context, attrs, view);

    return view;
}


/**
 * Invoke low-level function for instantiating a view by name. This attempts to
 * instantiate a view class of the given <var>name</var> found in this
 * LayoutInflater's ClassLoader.
 *
 * @param context
 * @param name    The full name of the class to be instantiated.
 * @param attrs   The XML attributes supplied for this instance.
 * @return View The newly instantiated view, or null.
 */
private View createView(Context context, String name, AttributeSet attrs) {
    View view = null;
    try {
        if (-1 == name.indexOf('.')) {
            view = createViewFromPrefix(context, name, "android.view.", attrs);
            if (view == null) {
                view=createViewFromPrefix(context, name, "android.widget.", attrs);
                if(view==null){
                    view= createViewFromPrefix(context, name, "android.webkit.", attrs);
                }
            }

        } else {
            L.i("自定義View to create " + name);
            view=createViewFromPrefix(context, name, null, attrs);
        }

    } catch (Exception e) {
        L.e("error while create 【" + name + "】 : " + e.getMessage());
        view = null;
    }
    return view;
}

private View createViewFromPrefix(Context context, String name, String prefix, AttributeSet attrs) {
    View view;
    try {
        view = createView(context, name, prefix, attrs);
    } catch (Exception e) {
        view = null;
    }
    return view;

}

public void applySkin() {
    if (ListUtils.isEmpty(mSkinItems)) {
        return;
    }

    for (SkinItem si : mSkinItems) {
        if (si.view == null) {
            continue;
        }
        si.apply();
    }
  }

public void addSkinView(SkinItem item) {
    mSkinItems.add(item);
 }

}

對View屬性進(jìn)行識別并轉(zhuǎn)化為皮膚屬性實(shí)體

  /**
     * Collect skin able tag such as background , textColor and so on
     *
     * @param context
     * @param attrs
     * @param view
     */
    private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
        List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();

        for (int i = 0; i < attrs.getAttributeCount(); i++) {
        String attrName = attrs.getAttributeName(i);
        String attrValue = attrs.getAttributeValue(i);

        if (!AttrFactory.isSupportedAttr(attrName)) {
            continue;
        }

        if (attrValue.startsWith("@")) {
            try {
                int id = Integer.parseInt(attrValue.substring(1));
                String entryName = context.getResources().getResourceEntryName(id);
                String typeName = context.getResources().getResourceTypeName(id);
                SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
                if (mSkinAttr != null) {
                    viewAttrs.add(mSkinAttr);
                }
            } catch (NumberFormatException e) {
                e.printStackTrace();
            } catch (NotFoundException e) {
                e.printStackTrace();
            }
          }
        }

        if (!ListUtils.isEmpty(viewAttrs)) {
        SkinItem skinItem = new SkinItem();
        skinItem.view = view;
        skinItem.attrs = viewAttrs;

        mSkinItems.add(skinItem);

        if (SkinManager.getInstance().isExternalSkin()) {
            skinItem.apply();
            }
        }

下面通過skin:enbale="true"這種方式丧没,對布局中需要換膚的View進(jìn)行標(biāo)記

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:skin="http://schemas.android.com/android/skin"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  android:background="@color/hall_back_color"
  skin:enable="true"
  >

<code.solution.widget.CustomActivityBar
    android:id="@+id/custom_activity_bar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/widget_action_bar_height"
    app:common_activity_title="@string/app_name"
    app:common_activity_title_gravity="center"
    app:common_activity_title_icon="@drawable/ic_win_cp"
    />
</LinearLayout>

在SKinInflaterFactory的onCreateView 方法中鹰椒,實(shí)際是對xml中映射的每個(gè)View
進(jìn)行過濾锡移。如果skin:enbale不為true則直接返回null交給系統(tǒng)默認(rèn)去創(chuàng)建呕童。而如果為true,則自己去創(chuàng)建這個(gè)View,并將這個(gè)VIew的所有屬性比如id, width height淆珊,textColor,background等與支持換膚的屬性進(jìn)行對比夺饲。比如我們支持換background textColor listSelector等, android:background="@color/hall_back_color" 這個(gè)屬性施符,在進(jìn)行換膚的時(shí)候往声,如果皮膚包里存在hall_back_color這個(gè)值的設(shè)置,就將這個(gè)顏色值替換為皮膚包里的顏色值戳吝,以完成換膚的需求浩销。同時(shí),也會將這個(gè)需要換膚的View保存起來听哭。

如果在切換換膚之后慢洋,進(jìn)入一個(gè)新的頁面,就在進(jìn)入這個(gè)頁面Activity的 InlfaterFacory的onCreateView里根據(jù)skin:enable="true" 這個(gè)標(biāo)記陆盘,進(jìn)行判斷普筹。為true則進(jìn)行換膚操作。而對于切換換膚操作時(shí)隘马,已經(jīng)存在的頁面太防,就對這幾個(gè)存在頁面保存好的需要換膚的View進(jìn)行換膚操作。

2)在代碼中動態(tài)添加的View

上述是針對在布局中設(shè)置skin:ebable="true"的View進(jìn)行換膚酸员,那么如果我們的View不是通過布局文件蜒车,而是通過在代碼種創(chuàng)建的View讳嘱,怎樣換膚呢?

public void dynamicAddSkinEnableView(Context context, View view, List<DynamicAttr> pDAttrs) {
    List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
    SkinItem skinItem = new SkinItem();
    skinItem.view = view;

    for (DynamicAttr dAttr : pDAttrs) {
        int id = dAttr.refResId;
        String entryName = context.getResources().getResourceEntryName(id);
        String typeName = context.getResources().getResourceTypeName(id);
        SkinAttr mSkinAttr = AttrFactory.get(dAttr.attrName, id, entryName, typeName);
        viewAttrs.add(mSkinAttr);
    }

    skinItem.attrs = viewAttrs;
    skinItem.apply();
    addSkinView(skinItem);
}

public void dynamicAddSkinEnableView(Context context, View view, String attrName, int attrValueResId)     {
    int id = attrValueResId;
    String entryName = context.getResources().getResourceEntryName(id);
    String typeName = context.getResources().getResourceTypeName(id);
    SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
    SkinItem skinItem = new SkinItem();
    skinItem.view = view;
    List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
    viewAttrs.add(mSkinAttr);
    skinItem.attrs = viewAttrs;
    skinItem.apply();
    addSkinView(skinItem);
}

即在Activity中通過比如
dynamicAddSkinEnableView(context, mTextView酿愧,"textColor",R.color.main_text_color)即可完成對動態(tài)創(chuàng)建的View的換膚操作呢燥。

本文研究是基于github開源項(xiàng)目Android-Skin-Loader進(jìn)行的。這個(gè)框架主要是動態(tài)加載皮膚包寓娩,在不需要重啟應(yīng)用的前提下叛氨,實(shí)現(xiàn)對頁面布局等動態(tài)換膚的操作。皮膚包獨(dú)立制作和維護(hù)棘伴,不和主工程產(chǎn)生耦合寞埠。同時(shí)由后臺服務(wù)器下發(fā),可即時(shí)在線更新不依賴客戶端版本焊夸。

皮膚包的加載過程:

SKinManger:

public void load(String skinPackagePath, final ILoaderListener callback) {
    
    new AsyncTask<String, Void, Resources>() {

        protected void onPreExecute() {
            if (callback != null) {
                callback.onStart();
            }
        };

        @Override
        protected Resources doInBackground(String... params) {
            try {
                if (params.length == 1) {
                    String skinPkgPath = params[0];
                    
                    File file = new File(skinPkgPath); 
                    if(file == null || !file.exists()){
                        return null;
                    }
                    
                    PackageManager mPm = context.getPackageManager();
                    PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
                    skinPackageName = mInfo.packageName;

                    AssetManager assetManager = AssetManager.class.newInstance();
                    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                    addAssetPath.invoke(assetManager, skinPkgPath);

                    Resources superRes = context.getResources();
                    Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
                    
                    SkinConfig.saveSkinPath(context, skinPkgPath);
                    
                    skinPath = skinPkgPath;
                    isDefaultSkin = false;
                    return skinResource;
                }
                return null;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        };

        protected void onPostExecute(Resources result) {
            mResources = result;

            if (mResources != null) {
                if (callback != null) callback.onSuccess();
                notifySkinUpdate();
            }else{
                isDefaultSkin = true;
                if (callback != null) callback.onFailed();
            }
        };

    }.execute(skinPackagePath);
}

@Override
public void attach(ISkinUpdate observer) {
    if(skinObservers == null){
        skinObservers = new ArrayList<ISkinUpdate>();
    }
    if(!skinObservers.contains(observer)){
        skinObservers.add(observer);
    }
}

@Override
public void detach(ISkinUpdate observer) {
    if(skinObservers == null) return;
    if(skinObservers.contains(observer)){
        skinObservers.remove(observer);
    }
}

@Override
public void notifySkinUpdate() {
    if(skinObservers == null) return;
    for(ISkinUpdate observer : skinObservers){
        observer.onThemeUpdate();
    }
}

SKinManager為整個(gè)皮膚包的管理類仁连,負(fù)責(zé)加載皮膚包文件,并得到該皮膚包的包名skinPackageName阱穗,和這個(gè)皮膚包的Resource對象skinResource饭冬,這樣整個(gè)皮膚包的資源文件我們就都可以拿到了。在加載得到皮膚包的Resource之后揪阶,通知每個(gè)注冊過(attach)的頁面(Activity)昌抠,去刷新這些頁面所有保存過的需要換膚的View,進(jìn)行換膚操作鲁僚。

切換時(shí)如何即時(shí)更新界面:

1炊苫、SkinBaseApplication:

public class SkinApplication extends BaseApplication {

@Override
public void onCreate() {
    super.onCreate();
    SkinManager.getInstance().init(this);
    SkinManager.getInstance().load();
  }
}

主要是進(jìn)行一些初始化的操作。

2冰沙、SkinBaseActivity:

public abstract class BaseActivity extends
    code.solution.base.BaseActivity implements ISkinUpdate, IDynamicNewView {

private SkinInflaterFactory mSkinInflaterFactory;

@Override
protected void onCreate(Bundle savedInstanceState) {

mSkinInflaterFactory = new SkinInflaterFactory();
LayoutInflaterCompat.setFactory(getLayoutInflater(), mSkinInflaterFactory);
super.onCreate(savedInstanceState);
changeStatusColor();
}

/**
 * dynamic add a skin view
 *
 * @param view
 * @param attrName
 * @param attrValueResId
 */
protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){
    mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
}

@Override
public void onThemeUpdate() {
    if(!isResponseOnSkinChanging){
        return;
    }
    mSkinInflaterFactory.applySkin();
    changeStatusColor();
}

在這里使用了之前自定義的SkinInflaterFactory侨艾,來替換默認(rèn)的Factory,以達(dá)到截獲創(chuàng)建View,獲取View的屬性拓挥,與支持換膚的屬性進(jìn)行對比唠梨,進(jìn)行View換膚操作以及保存這些需要換膚的View到List<SkinItem>中,在下次換膚切換時(shí)對這些View進(jìn)行換膚的目的侥啤。

其中換膚操作執(zhí)行時(shí)当叭,會調(diào)用SKinManager.notifySKinUpdate方法

@Override
public void notifySkinUpdate() {
    if(skinObservers == null) return;
    for(ISkinUpdate observer : skinObservers){
        observer.onThemeUpdate();
    }
}

而這里的observer.onThemeUpdate里面主要是執(zhí)行這個(gè)Activity的下述方法:

public void onThemeUpdate() {
    if(!isResponseOnSkinChanging){
        return;
    }
    mSkinInflaterFactory.applySkin();
    changeStatusColor();
}

mSkinInflaterFactory.applySkin();即為SKinInflaterFactory的applySkin方法,

public void applySkin() {
    if (ListUtils.isEmpty(mSkinItems)) {
        return;
    }

    for (SkinItem si : mSkinItems) {
        if (si.view == null) {
            continue;
        }
        si.apply();
    }
  }

其中 mSKinItems即為當(dāng)前Acitivty通過xml 文件中skin:enbale進(jìn)行標(biāo)記的 及動態(tài)dynamicAddSkinEnableView(...)添加的需要換膚的View的集合愿棋,這樣整個(gè)換膚的過程就完成了科展。

整體換膚框架類圖:

換膚架構(gòu)類圖.png

如何制作皮膚包:

1). 新建工程project
2). 將換膚的資源文件添加到res文件下,無java文件
3). 直接運(yùn)行build.gradle糠雨,生成apk文件(注意才睹,運(yùn)行時(shí)Run/Redebug configurations 中Launch Options選擇launch nothing),否則build 會報(bào) no default Activty的錯(cuò)誤。
4). 將apk文件重命名如black.apk琅攘,重命名為black.skin防止用戶點(diǎn)擊安裝

在線換膚:

  1. 將皮膚包上傳到服務(wù)器后臺
  2. 客戶端根據(jù)接口數(shù)據(jù)下載皮膚包垮庐,進(jìn)行加載及客戶端換膚操作

結(jié)語:

至此,整個(gè)換膚流程的原理解析已經(jīng)全部講完了坞琴。本文針對基本的換膚原理流程做了解析哨查,初步建立了一套相對完善的換膚框架。但是如何建立一套更加完善更加對其他開發(fā)者友善的換膚機(jī)制仍然是可以繼續(xù)研究的方向剧辐。比如如何更加安全的換膚寒亥,如何對代碼的侵入性做到最小(比如通過在配置文件中配置需要換膚的View的id name 而不是通過在xml文件中進(jìn)行標(biāo)記)等等荧关,都是可以繼續(xù)研究的方向溉奕,以后有時(shí)間會繼續(xù)在這方面進(jìn)行探索。

因時(shí)間關(guān)系文章難免有疏漏忍啤,歡迎提出指正加勤,謝謝。同時(shí)對換膚感興趣的童鞋可以參考以下鏈接:

1同波、Android-Skin-Loader
2鳄梅、Android-skin-support
3、Android主題換膚 無縫切換

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末未檩,一起剝皮案震驚了整個(gè)濱河市戴尸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌讹挎,老刑警劉巖校赤,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異筒溃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)沾乘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門怜奖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人翅阵,你說我怎么就攤上這事歪玲。” “怎么了掷匠?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵滥崩,是天一觀的道長。 經(jīng)常有香客問我讹语,道長钙皮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮短条,結(jié)果婚禮上导匣,老公的妹妹穿的比我還像新娘。我一直安慰自己茸时,他們只是感情好贡定,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著可都,像睡著了一般缓待。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上渠牲,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天命斧,我揣著相機(jī)與錄音,去河邊找鬼嘱兼。 笑死国葬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的芹壕。 我是一名探鬼主播汇四,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼踢涌!你這毒婦竟也來了通孽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤睁壁,失蹤者是張志新(化名)和其女友劉穎背苦,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體潘明,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡行剂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钳降。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厚宰。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖遂填,靈堂內(nèi)的尸體忽然破棺而出铲觉,到底是詐尸還是另有隱情,我是刑警寧澤吓坚,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布撵幽,位于F島的核電站,受9級特大地震影響礁击,放射性物質(zhì)發(fā)生泄漏盐杂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一况褪、第九天 我趴在偏房一處隱蔽的房頂上張望撕贞。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽统阿。三九已至监婶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間齿桃,已是汗流浹背惑惶。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留短纵,地道東北人带污。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像香到,于是被迫代替她去往敵國和親鱼冀。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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

  • 今天再給大家?guī)硪黄韶洝?Android的主題換膚 悠就,可插件化提供皮膚包千绪,無需Activity的重啟直接實(shí)現(xiàn)無縫...
    _SOLID閱讀 99,464評論 147 1,120
  • 前言 Android換膚技術(shù)已經(jīng)是很久之前就已經(jīng)被成熟使用的技術(shù)了,然而我最近才在學(xué)習(xí)和接觸熱修復(fù)的時(shí)候才看到理卑。在...
    靜默加載閱讀 3,063評論 1 8
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,499評論 25 707
  • 我知道自己一直不斷地在計(jì)劃和放棄的程序中循環(huán)藐唠,沒有好的成果帆疟,但又從未間斷運(yùn)行。 我知道宇立,我為什么不能取得卓越的成績...
    柴柴cc閱讀 176評論 0 0
  • 今天要回復(fù)一個(gè)微信后臺的提問 hi妈嘹,我是一個(gè)剛畢業(yè)的新人柳琢。最近在糾結(jié),大公司和創(chuàng)業(yè)團(tuán)隊(duì)我該選哪一家润脸?待遇和福利都相...
    肥寒925閱讀 573評論 0 1