目錄介紹
- 1.最簡單創(chuàng)造方法
- 1.1 Snackbar作用
- 1.2 最簡單的創(chuàng)建
- 1.3 Snackbar消失的幾種方式
- 2.源碼分析
- 2.1 Snackbar的make方法源碼分析
- 2.2 對Snackbar屬性進行設(shè)置
- 2.3 Snackbar的show顯示與點擊消失
- 2.4 顯示和隱藏中動畫源碼分析
- 3.經(jīng)典總結(jié)
- 3.1 Snackbar和SnackbarManager類的設(shè)計
- 4.思考問題分析
- 4.1 Snackbar的設(shè)計思路
- 4.2 什么時候Snackbar顯示會導(dǎo)致FloatingActionButton上移
- 4.3 Snackbar控件show時為何從下往上移出來
- 4.4 為什么Snackbar總是顯示在最下面
- 4.5 Snackbar與吐司有何區(qū)別
- 5.Snackbar封裝庫
好消息
- 博客筆記大匯總【16年3月到至今】坟冲,包括Java基礎(chǔ)及深入知識點假夺,Android技術(shù)博客薪寓,Python學(xué)習(xí)筆記等等则拷,還包括平時開發(fā)中遇到的bug匯總部念,當(dāng)然也在工作之余收集了大量的面試題炮沐,長期更新維護并且修正包归,持續(xù)完善……開源的文件是markdown格式的痢艺!同時也開源了生活博客案淋,從12年起座韵,積累共計47篇[近20萬字],轉(zhuǎn)載請注明出處踢京,謝謝誉碴!
- 鏈接地址:https://github.com/yangchong211/YCBlogs
- 如果覺得好,可以star一下瓣距,謝謝黔帕!當(dāng)然也歡迎提出建議,萬事起于忽微蹈丸,量變引起質(zhì)變成黄!
- Snackbar封裝庫項目地址:https://github.com/yangchong211/YCDialog
-
02.Toast源碼深度分析
- 最簡單的創(chuàng)建,簡單改造避免重復(fù)創(chuàng)建逻杖,show()方法源碼分析奋岁,scheduleTimeoutLocked吐司如何自動銷毀的,TN類中的消息機制是如何執(zhí)行的荸百,普通應(yīng)用的Toast顯示數(shù)量是有限制的闻伶,用代碼解釋為何Activity銷毀后Toast仍會顯示,Toast偶爾報錯Unable to add window是如何產(chǎn)生的管搪,Toast運行在子線程問題虾攻,Toast如何添加系統(tǒng)窗口的權(quán)限等等
-
03.DialogFragment源碼分析
- 最簡單的使用方法铡买,onCreate(@Nullable Bundle savedInstanceState)源碼分析,重點分析彈窗展示和銷毀源碼霎箍,使用中show()方法遇到的IllegalStateException分析
-
05.PopupWindow源碼分析
- 顯示PopupWindow奇钞,注意問題寬和高屬性,showAsDropDown()源碼漂坏,dismiss()源碼分析景埃,PopupWindow和Dialog有什么區(qū)別?為何彈窗點擊一下就dismiss呢顶别?
-
06.Snackbar源碼分析
- 最簡單的創(chuàng)建谷徙,Snackbar的make方法源碼分析,Snackbar的show顯示與點擊消失源碼分析驯绎,顯示和隱藏中動畫源碼分析完慧,Snackbar的設(shè)計思路,為什么Snackbar總是顯示在最下面
-
07.彈窗常見問題
- DialogFragment使用中show()方法遇到的IllegalStateException,什么常見產(chǎn)生的剩失?Toast偶爾報錯Unable to add window屈尼,Toast運行在子線程導(dǎo)致崩潰如何解決?
1.最簡單創(chuàng)造方法
1.1 Snackbar作用
- Snackbar是Android支持庫中用于顯示簡單消息并且提供和用戶的一個簡單操作的一種彈出式提醒拴孤。當(dāng)使用Snackbar時脾歧,提示會出現(xiàn)在消息最底部,通常含有一段信息和一個可點擊的按鈕演熟。
- 同樣作為消息提示鞭执,Snackbar相比于Toast而言,增加了一個用戶操作芒粹,并且在同時彈出多個消息時兄纺,Snackbar會停止前一個,直接顯示后一個化漆,也就是說同一時刻只會有一個Snackbar在顯示囤热;而Toast則不然,如果不做特殊處理获三,那么同時可以有多個Toast出現(xiàn);Snackbar相比于Dialog锨苏,操作更少疙教,因為只有一個用戶操作的接口,而Dialog最多可以設(shè)置三個伞租,另外Snackbar的出現(xiàn)并不影響用戶的繼續(xù)操作贞谓,而Dialog則必須需要用戶做出響應(yīng),所以相比Dialog葵诈,Snackbar更輕量裸弦。
1.2 最簡單的創(chuàng)建
- 如下所示
Snackbar sb = Snackbar.make(v,"瀟湘劍雨",Snackbar.LENGTH_LONG)
.setAction("刪除嗎祟同?", new View.OnClickListener() {
@Override
public void onClick(View v) {
//點擊了"是嗎?"字符串操作
ToastUtils.showRoundRectToast("逗比");
}
})
.setActionTextColor(Color.RED)
.setText("楊充是個逗比")
.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
super.onDismissed(transientBottomBar, event);
switch (event) {
case Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE:
case Snackbar.Callback.DISMISS_EVENT_MANUAL:
case Snackbar.Callback.DISMISS_EVENT_SWIPE:
case Snackbar.Callback.DISMISS_EVENT_TIMEOUT:
ToastUtils.showRoundRectToast("刪除成功");
break;
case Snackbar.Callback.DISMISS_EVENT_ACTION:
ToastUtils.showRoundRectToast("撤銷了刪除操作");
break;
}
Log.d("MainActivity","onDismissed");
}
@Override
public void onShown(Snackbar transientBottomBar) {
super.onShown(transientBottomBar);
Log.d("MainActivity","onShown");
}
});
sb.show();
1.3 Snackbar消失的幾種方式
- Snackbar顯示只有一種方式理疙,那就是調(diào)用show()方法晕城,但是消失有幾種方式:時間到了自動消失、點擊了右側(cè)按鈕消失窖贤、新的Snackbar出現(xiàn)導(dǎo)致舊的Snackbar消失砖顷、滑動消失或者通過調(diào)用dismiss()消失。
- 分別對應(yīng)于Snackbar.Callback中的幾個常量值赃梧。
- DISMISS_EVENT_ACTION:點擊了右側(cè)按鈕導(dǎo)致消失
- DISMISS_EVENT_CONSECUTIVE:新的Snackbar出現(xiàn)導(dǎo)致舊的消失
- DISMISS_EVENT_MANUAL:調(diào)用了dismiss方法導(dǎo)致消失
- DISMISS_EVENT_SWIPE:滑動導(dǎo)致消失
- DISMISS_EVENT_TIMEOUT:設(shè)置的顯示時間到了導(dǎo)致消失
- Callback有兩個方法
- void onDismissed(B transientBottomBar, @DismissEvent int event)
- void onShown(B transientBottomBar)
- 其中onShown在Snackbar可見時調(diào)用滤蝠,onDismissed在Snackbar準(zhǔn)備消失時調(diào)用。
- 分別對應(yīng)于Snackbar.Callback中的幾個常量值赃梧。
2.源碼分析
2.1 Snackbar的make方法源碼分析
- 創(chuàng)建Snackbar需要使用靜態(tài)的make方法授嘀,并且其中的view參數(shù)是一個查找父布局的起點
- 這里可以看到物咳,snackBar的布局是design_layout_snackbar_include,假如我們需要自定義SnackBar并且設(shè)置字體顏色蹄皱,大小等屬性览闰。則需要拿到這個布局的控件id等。關(guān)于封裝庫夯接,可以查看:https://github.com/yangchong211/YCDialog
- image
- 其中findSuitableParent()方法為以view為起點尋找合適的父布局焕济,下面看看findSuitableParent()如何做的?
- 看了下面源碼可知:可以看到如果view是CoordinatorLayout盔几,那么就直接作為父布局了晴弃;如果是FrameLayout,并且如果是android.R.id.content逊拍,也就是查找到了DecorView上鞠,即最頂部,那么就只用這個view芯丧;如果不是的話芍阎,先保存下來;接下來就是獲取view的父布局缨恒,然后循環(huán)再次判斷谴咸。這樣導(dǎo)致的結(jié)果最終會有兩個選擇,要么是CoordinatorLayout骗露,要么就是FrameLayout岭佳,并且是最頂層的那個布局。
- 如果從View往上搜尋萧锉,如果有CoordinatorLayout珊随,那么就使用該CoordinatorLayout ;如果從View往上搜尋,沒有CoordinatorLayout叶洞,那么就使用android.R.id.content的FrameLayout
- image
2.2 對Snackbar屬性進行設(shè)置
-
2.2.1 setActionTextColor設(shè)置action顏色
- 可以看到先是獲取父布局contentLayout鲫凶,然后在獲取snackbar_action的mActionView
@NonNull public Snackbar setActionTextColor(@ColorInt int color) { final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0); final TextView tv = contentLayout.getActionView(); tv.setTextColor(color); return this; } //然后看SnackbarContentLayout類中g(shù)etActionView方法 @Override protected void onFinishInflate() { super.onFinishInflate(); mMessageView = (TextView) findViewById(R.id.snackbar_text); mActionView = (Button) findViewById(R.id.snackbar_action); } public Button getActionView() { return mActionView; }
-
2.2.2 看setAction()方法的實現(xiàn)
- 首先是獲取父布局contentLayout,然后通過contentLayout調(diào)用getActionView()方法衩辟,返回的tv其實就是右邊的Button螟炫,然后判斷文本和監(jiān)聽器,設(shè)置可見性惭婿、文本不恭、監(jiān)聽器。
@NonNull public Snackbar setAction(CharSequence text, final View.OnClickListener listener) { final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0); final TextView tv = contentLayout.getActionView(); if (TextUtils.isEmpty(text) || listener == null) { tv.setVisibility(View.GONE); tv.setOnClickListener(null); } else { tv.setVisibility(View.VISIBLE); tv.setText(text); tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { listener.onClick(view); // Now dismiss the Snackbar dispatchDismiss(BaseCallback.DISMISS_EVENT_ACTION); } }); } return this; }
2.3 Snackbar的show顯示與點擊消失
- 2.3.1 show顯示
- 可以看到财饥,首先獲取一個SnackbarManager對象换吧,然后調(diào)用它的show方法≡啃牵可以看到在這個方法中沾瓦,先判斷如果是當(dāng)前正在顯示的SnackBar對應(yīng)的CallBack,則更新顯示時長谦炒,然后從消息隊列中移除贯莺,最后調(diào)用scheduleTimeoutLocked方法發(fā)送定時消息dismiss;如果是下一個要顯示的宁改,則更新顯示時長缕探;如果都不是,那么就創(chuàng)建一個SnackbarRecord對象还蹲。
- isCurrentSnackbarLocked:如果當(dāng)前已經(jīng)有一個Snackbar顯示了爹耗,又再調(diào)用了該對象的show方法,但是只是設(shè)置了不同時間谜喊,那么isCurrentSnackbarLocked就會是true潭兽,執(zhí)行里面的方法。
- isNextSnackbarLocked:如果當(dāng)前已有一個Snackbar正在顯示斗遏,又創(chuàng)建了一個新的Snackbar并調(diào)用show方法山卦,則執(zhí)行這個條件代碼
- 如果兩條件都不成立,則需要創(chuàng)建一個新記錄并對其進行排隊诵次。
public void show() { SnackbarManager.getInstance().show(mDuration, mManagerCallback); } public void show(int duration, Callback callback) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback)) { // 表示回調(diào)已在隊列中账蓉。我們只需更新持續(xù)時間 mCurrentSnackbar.duration = duration; // 如果這是當(dāng)前正在顯示的Snackbar,請調(diào)用重新調(diào)度它的 // timeout mHandler.removeCallbacksAndMessages(mCurrentSnackbar); // 這個方法很重要逾一,當(dāng)執(zhí)行時間結(jié)束后剔猿,就會自動dismiss。下面再詳細(xì)分析 scheduleTimeoutLocked(mCurrentSnackbar); return; } else if (isNextSnackbarLocked(callback)) { //我們只需更新持續(xù)時間 mNextSnackbar.duration = duration; } else { //否則嬉荆,我們需要創(chuàng)建一個新記錄并對其進行排隊。 mNextSnackbar = new SnackbarRecord(duration, callback); } if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) { // 如果我們目前有一個Snackbar酷含,請嘗試取消它并排隊等待鄙早。 return; } else { // 清除當(dāng)前的快捷鍵 mCurrentSnackbar = null; //很重要 showNextSnackbarLocked(); } } } //注意這個callback方法 final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { @Override public void show() { sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this)); } @Override public void dismiss(int event) { sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, BaseTransientBottomBar.this)); } }; //處理sHandler發(fā)送的消息 static { sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_SHOW: ((BaseTransientBottomBar) message.obj).showView(); return true; case MSG_DISMISS: ((BaseTransientBottomBar) message.obj).hideView(message.arg1); return true; } return false; } }); }
- 然后看看showNextSnackbarLocked這個方法汪茧,注意:mCurrentSnackbar當(dāng)前正在顯示的,而mNextSnackbar是下一個要顯示的限番。能看到會調(diào)用callback的show方法舱污,而這個calllback對象就是我們在調(diào)用snackbar的show方法是傳進去的那個。向Snackbar的Handler發(fā)送一個消息弥虐,最后顯示Snackbar扩灯。
private void showNextSnackbarLocked() { if (mNextSnackbar != null) { mCurrentSnackbar = mNextSnackbar; mNextSnackbar = null; final Callback callback = mCurrentSnackbar.callback.get(); if (callback != null) { callback.show(); } else { // The callback doesn't exist any more, clear out the Snackbar mCurrentSnackbar = null; } } }
- 2.3.2 看看scheduleTimeoutLocked源碼如何銷毀snackBar
- 可以發(fā)現(xiàn),如果我們設(shè)置為無限期霜瘪,則不會設(shè)置超時珠插,直接return函數(shù)。然后發(fā)送了一個叫做MSG_TIMEOUT的消息颖对,繼續(xù)追終捻撑,最后會到達(dá)cancelSnackbarLocked方法。在cancelSnackbarLocked這個方法中缤底,首先移除SnackbarRecord發(fā)出的所有消息顾患,然后調(diào)用Callback的dismiss方法,從上面我們知道最終是向Snackbar的sHandler發(fā)送了一條消息个唧,最終是調(diào)用Snackbar的hideView消失江解。
private void scheduleTimeoutLocked(SnackbarRecord r) { if (r.duration == Snackbar.LENGTH_INDEFINITE) { // If we're set to indefinite, we don't want to set a timeout return; } int durationMs = LONG_DURATION_MS; if (r.duration > 0) { durationMs = r.duration; } else if (r.duration == Snackbar.LENGTH_SHORT) { durationMs = SHORT_DURATION_MS; } mHandler.removeCallbacksAndMessages(r); mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs); } //接受mHandler消息并且處理 mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_TIMEOUT: handleTimeout((SnackbarRecord) message.obj); return true; } return false; } }); // void handleTimeout(SnackbarRecord record) { synchronized (mLock) { if (mCurrentSnackbar == record || mNextSnackbar == record) { cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT); } } } //最終可以追蹤到這個方法 private boolean cancelSnackbarLocked(SnackbarRecord record, int event) { final Callback callback = record.callback.get(); if (callback != null) { // Make sure we remove any timeouts for the SnackbarRecord mHandler.removeCallbacksAndMessages(record); callback.dismiss(event); return true; } return false; }
2.4 顯示和隱藏中動畫源碼分析
- 在顯示的時候是這樣設(shè)置動畫的,具體如下所示
- image
- 在隱藏的時候是這樣設(shè)置動畫的徙歼,具體如下所示
- image
- 最后具體看一下animateViewOut部分源碼
- 可以看到在動畫結(jié)束的最后都調(diào)用了onViewHidden方法犁河,所以最終都是要調(diào)用onViewHidden方法的。
private void animateViewOut(final int event) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { ViewCompat.animate(mView) .translationY(mView.getHeight()) .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) .setDuration(ANIMATION_DURATION) .setListener(new ViewPropertyAnimatorListenerAdapter() { @Override public void onAnimationStart(View view) { mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION); } @Override public void onAnimationEnd(View view) { onViewHidden(event); } }).start(); } else { Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_out); anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR); anim.setDuration(ANIMATION_DURATION); anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationEnd(Animation animation) { onViewHidden(event); } @Override public void onAnimationStart(Animation animation) {} @Override public void onAnimationRepeat(Animation animation) {} }); mView.startAnimation(anim); } }
- onViewHidden提供具體的業(yè)務(wù)處理,具體如下所示
- 首先調(diào)用SnackbarManager的onDismissed方法鲁沥,然后判斷Snackbar.Callback是不是null呼股,調(diào)用Snackbar.Callback的onDismissed方法,就是我們上面介紹的處理Snackbar消失的方法画恰。最后就是將Snackbar的mView移除彭谁。
- image
3.經(jīng)典總結(jié)
3.1 Snackbar和SnackbarManager類的設(shè)計
- Snackbar和SnackbarManager,SnackbarManager內(nèi)部有兩個SnackbarRecord允扇,一個mCurrentSnackbar缠局,一個mNextSnackbar,SnackbarManager通過這兩個對象實現(xiàn)Snackbar的順序顯示考润,如果在一個Snackbar顯示之前有Snackbar正在顯示狭园,那么使用mNextSnackbar保存第二個Snackbar,然后讓第一個Snackbar消失糊治,然后消失之后再調(diào)用SnackbarManager顯示下一個Snackbar唱矛,如此循環(huán),實現(xiàn)了Snackbar的順序顯示。
- Snackbar負(fù)責(zé)顯示和消失绎谦,具體來說其實就是添加和移除View的過程管闷。Snackbar和SnackbarManager的設(shè)計很巧妙,利用一個SnackbarRecord對象保存Snackbar的顯示時間以及SnackbarManager.Callback對象窃肠,前面說到每一個Snackbar都有一個叫做mManagerCallback的SnackbarManager.Callback對象包个,下面看一下SnackRecord類的定義:
- image
- Snackbar向SnackbarManager發(fā)送消息主要是調(diào)用SnackbarManager.getInstace()返回一個單例對象;而SnackManager向Snackbar發(fā)送消息就是通過show方法傳入的Callback對象冤留。SnackbarManager中的Handler只處理一個MSG_TIMEOUT事件碧囊,最后是調(diào)用Snackbar的hideView消失的;Snackbar的sHandler處理兩個消息纤怒,showView和hideView糯而,而消息的發(fā)送者是mManagerCallback,控制者是SnackbarManager肪跋。
4.思考問題分析
4.1 Snackbar的設(shè)計思路
- 具體可以看經(jīng)典總結(jié)3.1
4.2 什么時候Snackbar顯示會導(dǎo)致FloatingActionButton上移
- 為什么CoordinatorLayout + FloatingActionButton歧蒋,當(dāng)Snackbar顯示的時候FloatingActionButton會上移呢,這個是怎么實現(xiàn)的州既?
- 把CoordinatorLayout替換成FrameLayout確不行谜洽。這個問題我們還沒說。其實這個不是在Snackbar里面處理的吴叶,是通過CoordinatorLayout和Behavior來處理的阐虚。那具體的處理在哪里呢。FloatingActionButton類里面Behavior類。正是Behavior里面的兩個函數(shù)layoutDependsOn()和onDependentViewChanged()函數(shù)作用的結(jié)果。直接進去看下FloatingActionButton內(nèi)部類Behavior里面這兩個函數(shù)的代碼记靡。
4.3 Snackbar控件show時為何從下往上移出來
- 至于說Snackbar控件show時為何從下往上移出來,看下面這段代碼就知道呢咸灿,如下所示
- image
4.4 為什么Snackbar總是顯示在最下面
- 直接找到make方法中的填充布局,然后去看design_layout_snackbar_include的布局參數(shù)侮叮,結(jié)果如下:
- image
4.5 Snackbar與吐司有何區(qū)別
- 與Toast進行比較避矢,SnackBar有優(yōu)勢:
- 1.SnackBar可以自動消失,也可以手動取消(側(cè)滑取消囊榜,但是需要在特殊的布局中审胸,后面會仔細(xì)說)
- 2.SnackBar可以通過setAction()來與用戶進行交互
- 3.通過CallBack我們可以獲取SnackBar的狀態(tài)
- image
5.Snackbar封裝庫
- 可以一行代碼調(diào)用,也可以自己使用鏈?zhǔn)骄幊陶{(diào)用卸勺。支持設(shè)置顯示時長屬性砂沛;可以設(shè)置背景色;可以設(shè)置文字大小曙求,顏色映企;可以設(shè)置action內(nèi)容卑吭,文字大小,顏色富稻,還有點擊事件椭赋;可以設(shè)置icon哪怔;代碼如下所示认境,更多內(nèi)容可以直接運行demo哦叉信!
//1.只設(shè)置text SnackBarUtils.showSnackBar(this,"滾犢子"); //2.設(shè)置text硼身,action,和點擊事件 SnackBarUtils.showSnackBar(this, "滾犢子", "ACTION", new View.OnClickListener() { @Override public void onClick(View v) { ToastUtils.showRoundRectToast("滾犢子啦丑罪?"); } }); //3.設(shè)置text巍糯,action祟峦,和點擊事件宅楞,和icon SnackBarUtils.showSnackBar(this, "滾犢子", "ACTION",R.drawable.icon_cancel, new View.OnClickListener() { @Override public void onClick(View v) { ToastUtils.showRoundRectToast("滾犢子啦距淫?"); } }); //4.鏈?zhǔn)秸{(diào)用 SnackBarUtils.builder() .setBackgroundColor(this.getResources().getColor(R.color.color_7f000000)) .setTextSize(14) .setTextColor(this.getResources().getColor(R.color.white)) .setTextTypefaceStyle(Typeface.BOLD) .setText("滾犢子") .setMaxLines(4) .centerText() .setActionText("收到") .setActionTextColor(this.getResources().getColor(R.color.color_f25057)) .setActionTextSize(16) .setActionTextTypefaceStyle(Typeface.BOLD) .setActionClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ToastUtils.showRoundRectToast("滾犢子啦?"); } }) .setIcon(R.drawable.icon_cancel) .setActivity(MainActivity.this) .setDuration(SnackBarUtils.DurationType.LENGTH_INDEFINITE) .build() .show();
關(guān)于其他內(nèi)容介紹
01.關(guān)于博客匯總鏈接
- 1.技術(shù)博客匯總
- 2.開源項目匯總
- 3.生活博客匯總
- 4.喜馬拉雅音頻匯總
- 5.其他匯總
02.關(guān)于我的博客
- 我的個人站點:www.yczbj.org彤枢,www.ycbjie.cn
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yang-chong-69-24/pins/posts
- 簡書:http://www.reibang.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜馬拉雅聽書:http://www.ximalaya.com/zhubo/71989305/
- 開源中國:https://my.oschina.net/zbj1618/blog
- 泡在網(wǎng)上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 郵箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault頭條:https://segmentfault.com/u/xiangjianyu/articles