Android 自定義Binding Adapter實戰(zhàn)應(yīng)用及作用詳解
1. Binding Adapter 的基本概念和作用
Binding Adapter 是一個橋梁,它允許你在 XML 布局文件中,將自定義的屬性(例如 app:image_corners_uri)與 Kotlin/Java 代碼中的方法綁定起來。它解決了 Android 默認(rèn)的 Data Binding 無法處理復(fù)雜邏輯或自定義 View 屬性的問題。
class DataBindingAdapter {
companion object {
@BindingAdapter("media_source_icon", "media_source_name")
@JvmStatic
fun setMediaSourceInfo(view: MediaSourceBar, drawable: Drawable?, name: String?) {
name?.let { view.setMediaSourceName(it) }
drawable?.let { view.setMediaSourceIcon(it) }
}
@BindingAdapter("media_source_right_button")
@JvmStatic
fun setMediaSourceRightButton(view: MediaSourceBar, rotate: Boolean?) {
rotate?.let { view.setMediaSourceRightButton(it) }
}
@BindingAdapter("data")
@JvmStatic
fun bindRecyclerView(
recyclerView: Recyclerview,
list: MutableList<ListItem>?,
) {
val adapter = recyclerView.adapter
if (adapter is RecyclerViewListAdapter) {
list?.let { adapter.submitList(it) }
}
}
@BindingAdapter(value = ["media_metadata", "media_source"], requireAll = false)
@JvmStatic
fun bindMediaMetadataInfo(
simpleMediaView: PlayInfoView,
mediaMetadata: MediaMetadata?,
mediaSource: ServiceBean?
) {
simpleMediaView.setMediaMetadata(mediaMetadata)
if (mediaSource?.packageName in MediaBrowserManager.musicMediaSourceServices) {
simpleMediaView.setMediaServiceBean(mediaSource)
} else {
simpleMediaView.setMediaServiceBean(null)
}
}
@BindingAdapter("duration", "progress", requireAll = false)
@JvmStatic
fun bindMediaMetadataProgress(
simpleMediaView: PlayInfoView,
duration: Long?,
progress: Long?
) {
simpleMediaView.setProgress(duration, progress)
}
@BindingAdapter("player_commands", requireAll = false)
@JvmStatic
fun bindMediaMetadataCommands(
simpleMediaView: PlayInfoView,
commands: Player.Commands?,
) {
simpleMediaView.setPlayerCommands(commands)
}
@BindingAdapter("isPlaying", requireAll = false)
@JvmStatic
fun bindMediaMetadataPlayback(
view: View,
isPlaying: Boolean?,
) {
if (view is PlayInfoView) {
view.setPlayback(isPlaying)
}
if (view is tech.jidouauto.component.widgets.basic.ImageView) {
if (isPlaying == true) {
view.setImageResource(com.jidouauto.mediacenter.R.drawable.item_icon_pause)
} else {
view.setImageResource(com.jidouauto.mediacenter.R.drawable.item_icon_play)
}
}
}
@BindingAdapter("playMode", requireAll = false)
@JvmStatic
fun bindPlayMode(
simpleMediaView: PlayInfoView,
mode: PlayMode?
) {
if (mode != null) {
simpleMediaView.setPlayMode(mode)
}
}
@BindingAdapter("type")
@JvmStatic
fun setType(view: PlayInfoView, type: Int) {
view.setType(type)
}
@BindingAdapter("discType")
@JvmStatic
fun setDiscType(view: PlayInfoView, type: Int) {
view.setDiscType(type)
}
@BindingAdapter("image_corners_uri", "image_radius", requireAll = false)
@JvmStatic
fun imageUri(view: tech.jidouauto.component.widgets.basic.ImageView, uri: Any?, radius: Int?) {
uri?.let {
val imageResource = ImageResource.Remote(
uri,
transformationType = IImageLoader.ImageTransformationType.RoundCorners(radius?.dp ?: 24.dp),
placeholder = com.jidouauto.mediacenter.R.drawable.icon_placeholder,
error = com.jidouauto.mediacenter.R.drawable.icon_placeholder
)
view.imageSource = imageResource
}
}
@BindingAdapter("image_cycle_uri", requireAll = false)
@JvmStatic
fun cycleImageUri(view: tech.jidouauto.component.widgets.basic.ImageView, uri: Any?) {
uri?.let {
val imageResource = ImageResource.Remote(
uri,
transformationType = IImageLoader.ImageTransformationType.CircleCrop,
placeholder = com.jidouauto.mediacenter.R.drawable.icon_placeholder_circle,
error = com.jidouauto.mediacenter.R.drawable.icon_placeholder_circle
)
view.imageSource = imageResource
}
}
@BindingAdapter("player_album_image_cover", requireAll = false)
@JvmStatic
fun playerAlbumImageCover(
view: tech.jidouauto.component.widgets.basic.ImageView,
uri: Any?
) {
}
@BindingAdapter("banner_data", requireAll = false)
@JvmStatic
fun bindBannerViewData(
view: TopBannerCarouselPager,
data: MainBannerItem?,
) {
Logger.info("bind main banner data=${data?.items}")
if (data == null) return
view.setAdapter(TopBannerCarouselPager.Adapter(data.items, data.itemOnClick))
}
@BindingAdapter("show_background", requireAll = false)
@JvmStatic
fun showBackGround(view: View, isShow: Boolean) {
}
/**
* 控制Lottie動畫的播放狀態(tài)
* @param view LottieAnimationView實例
* @param isPlaying 是否正在播放(true:播放,false:暫停)
*/
@BindingAdapter("lottie_playing_state")
@JvmStatic
fun controlLottieAnimation(view: LottieAnimationView, isPlaying: Boolean?) {
isPlaying?.let {
if (it) {
// 如果需要播放且當(dāng)前未播放,則開始/恢復(fù)動畫
if (!view.isAnimating) {
view.playAnimation()
}
} else {
// 如果需要暫停且當(dāng)前正在播放,則暫停動畫
if (view.isAnimating) {
view.pauseAnimation()
}
}
}
}
/**
* 可選:控制Lottie動畫的可見性(根據(jù)播放狀態(tài))
* 當(dāng)播放時顯示動畫,暫停時隱藏
*/
@BindingAdapter("lottie_visible_with_playing")
@JvmStatic
fun setLottieVisibilityWithPlaying(view: LottieAnimationView, isPlaying: Boolean?) {
view.visibility = if (isPlaying == true) View.VISIBLE else View.GONE
}
}
} 在這份代碼中,@BindingAdapter 注解就是關(guān)鍵。它告訴 Data Binding 編譯器,當(dāng) XML 中使用了該注解中定義的屬性時,應(yīng)該調(diào)用被注解的方法。
例如:@BindingAdapter("media_source_icon", "media_source_name"):定義了兩個屬性,media_source_icon 和 media_source_name。當(dāng) XML 中同時使用這兩個屬性時,會調(diào)用 setMediaSourceInfo 方法。
2. 常見應(yīng)用場景與代碼分析
這份代碼覆蓋了多種常見的 Binding Adapter 應(yīng)用場景,我們可以逐一分析:
2.1. 綁定簡單屬性和多個屬性
setMediaSourceInfo 方法:
注解: @BindingAdapter("media_source_icon", "media_source_name")
應(yīng)用: 將 Drawable 和 String 類型的數(shù)據(jù)綁定到 MediaSourceBar 這個自定義 View 上,分別設(shè)置圖標(biāo)和名稱。
知識點: @BindingAdapter 注解可以接收多個屬性名,當(dāng)這些屬性都在 XML 中出現(xiàn)時,被注解的方法會被調(diào)用。方法參數(shù)的順序和類型必須與屬性值匹配。
setMediaSourceRightButton 方法:
注解: @BindingAdapter("media_source_right_button")
應(yīng)用: 將一個 Boolean 值綁定到 MediaSourceBar 的一個按鈕上,控制其旋轉(zhuǎn)狀態(tài)。
知識點: 展示了如何綁定單個自定義屬性,將一個布爾值直接傳遞給 View 的方法。
2.2. 處理列表數(shù)據(jù)綁定到 RecyclerView
bindRecyclerView 方法:
注解: @BindingAdapter("data")
應(yīng)用: 將一個 MutableList<ListItem> 數(shù)據(jù)列表直接綁定到 RecyclerView。
知識點: 這個方法非常實用,它通過檢查 RecyclerView 的 adapter 類型(確保是 RecyclerViewListAdapter),然后調(diào)用 adapter.submitList() 來更新數(shù)據(jù)。這在 ViewModel 中處理列表數(shù)據(jù)時非常方便,只需在 XML 中設(shè)置 app:data="@{viewModel.myList}",就能實現(xiàn)列表的自動更新。
2.3. 綁定復(fù)雜對象和多參數(shù)
bindMediaMetadataInfo 方法:
注解: @BindingAdapter(value = ["media_metadata", "media_source"], requireAll = false)
應(yīng)用: 將 MediaMetadata 和 ServiceBean 兩個復(fù)雜的對象綁定到 PlayInfoView 上。
知識點:
value = [...]:用于定義多個屬性。
requireAll = false:這是一個重要的參數(shù)。它表示這些屬性不必同時出現(xiàn)在 XML 中。如果設(shè)置為 true(默認(rèn)),只有當(dāng) XML 中同時包含了所有屬性時,該方法才會被調(diào)用。設(shè)置為 false 意味著只要其中一個屬性存在,方法就會被調(diào)用。
方法中的邏輯展示了如何根據(jù) mediaSource.packageName 進行條件判斷,然后設(shè)置不同的 View 狀態(tài),這是 Binding Adapter 中處理復(fù)雜邏輯的常見方式。
2.4. 綁定圖片加載庫
imageUri 和 cycleImageUri 方法:
注解: @BindingAdapter("image_corners_uri", "image_radius", requireAll = false) 和 @BindingAdapter("image_cycle_uri", requireAll = false)
應(yīng)用: 加載遠程圖片 URL(URI),并根據(jù)不同的屬性進行不同的處理。image_corners_uri 用于加載圓角圖片,而 image_cycle_uri 用于加載圓形圖片。
知識點: 這兩個方法是 Binding Adapter 最常見的用途之一。它們將圖片加載庫(如 Glide、Picasso 或你代碼中的 ImageResource.Remote)的復(fù)雜調(diào)用邏輯封裝起來,讓開發(fā)者在 XML 中只需簡單地提供一個 URL 即可,大大簡化了 View 的使用。同時,它還演示了如何處理占位符和錯誤圖片。
2.5. 條件判斷和多類型 View 處理
bindMediaMetadataPlayback 方法:
注解: @BindingAdapter("isPlaying", requireAll = false)
應(yīng)用: 根據(jù) isPlaying 的布爾值,改變不同 View 的狀態(tài)。
知識點:方法參數(shù)中的 View 類型非常通用。
方法內(nèi)部使用 if (view is PlayInfoView) 和 if (view is tech.jidouauto.component.widgets.basic.ImageView) 進行 類型判斷。這允許同一個 Binding Adapter 處理不同類型的 View,根據(jù) View 的具體類型執(zhí)行不同的邏輯。例如,如果是 PlayInfoView 就設(shè)置播放狀態(tài),如果是 ImageView 就改變圖標(biāo)。
3.@JvmStatic和companion object
@JvmStatic: 這是一個 Kotlin 注解,用于標(biāo)記一個方法為靜態(tài)方法。在 companion object 中使用此注解后,該方法可以像 Java 的靜態(tài)方法一樣,通過類名直接調(diào)用,這正是 Data Binding 編譯器所要求的。
companion object: Kotlin 中的伴生對象,用于存放與類相關(guān)的靜態(tài)成員。將所有 Binding Adapter 方法放在 companion object 中是 Kotlin 的最佳實踐。
OutlineProvider.kt
OutlineProvider 類是一個自定義的 ViewOutlineProvider,它的主要作用是為 View 設(shè)置圓角裁剪(clipping)效果。它接收 CornerType 和 radius 作為參數(shù),然后根據(jù)這些參數(shù)生成一個 Path,并用它來定義 View 的輪廓(outline)。這使得開發(fā)者可以在不修改 View 背景或使用其他復(fù)雜方法的情況下,輕松地為 View 的特定角或所有角設(shè)置圓角效果。
CornerType枚舉定義了九種不同的圓角類型,包括:ALL: 所有四個角都設(shè)置圓角。TOP_LEFT,TOP_RIGHT,BOTTOM_LEFT,BOTTOM_RIGHT: 僅設(shè)置單個角。TOP,BOTTOM,LEFT,RIGHT: 設(shè)置特定邊上的兩個角。
getOutline方法是核心邏輯所在,它根據(jù)cornerType和radius的值創(chuàng)建一個floatArray,該數(shù)組用于Path的addRoundRect方法來創(chuàng)建帶有指定圓角的矩形路徑。最后,它使用outline.setPath(path)將這個路徑應(yīng)用到 View 的輪廓上。
DatabindingAdapter.kt
DatabindingAdapter 類是一個包含大量靜態(tài) BindingAdapter 方法的集合,這些方法用于在 XML 布局文件中實現(xiàn)自定義屬性和數(shù)據(jù)綁定。它的核心作用是:
- 圖片加載: 提供了多種方法(如
setImageUrl、setCircleImageUrl、setIqiyiSpecifiedSizeImageUrl)來使用 Glide 庫加載圖片。這些方法支持處理不同的圖片資源類型(ID、Drawable、URI),設(shè)置占位圖、圓角半徑、高斯模糊等效果。 - UI 控件屬性設(shè)置: 提供了許多用于設(shè)置各種 UI 控件屬性的方法,例如:
bindTabLayoutTabs: 動態(tài)綁定和設(shè)置TabLayout的 Tab。setLayoutHeight,layoutMarginTop等: 設(shè)置 View 的布局參數(shù)(如高度、邊距)。setQrcodeStatus: 設(shè)置QrcodeScreenView的狀態(tài)和內(nèi)容。setXimalayaAlbumTags: 根據(jù)媒體項的數(shù)據(jù),動態(tài)顯示喜馬拉雅的VIP、付費、精品等標(biāo)簽圖標(biāo)。setIqyNormalModelTag: 根據(jù)媒體項數(shù)據(jù),動態(tài)顯示愛奇藝的VIP、獨家等標(biāo)簽圖標(biāo)。setOperationPanelItemActivedDisabled: 管理操作面板項的激活和禁用狀態(tài)。strikeThrough: 為TextView的文本添加刪除線效果。
- 動畫控制: 提供了用于控制 View 顯示和隱藏動畫的方法,例如
setPopupOpenAnimation、setCancelAnimation等。 - 邏輯封裝: 將復(fù)雜的邏輯(如根據(jù)媒體數(shù)據(jù)判斷顯示哪個標(biāo)簽圖標(biāo))封裝在 BindingAdapter 方法中,使得 XML 布局文件更加簡潔和易讀。
BindingResourceUtil.kt
BindingResourceUtil 是一個工具類,包含了一系列靜態(tài)方法,用于將數(shù)據(jù)模型中的原始值(如浮點數(shù)、字符串、枚舉)轉(zhuǎn)換為可用于數(shù)據(jù)綁定的資源對象(如 ImageResource、TextResource)。它的核心作用是:
- 數(shù)據(jù)到資源的轉(zhuǎn)換: 提供了將邏輯值轉(zhuǎn)換為 UI 資源的方法,例如:
playbackSpeedIcon: 根據(jù)播放速度的浮點值返回相應(yīng)的圖標(biāo)資源 ID。definitionText: 根據(jù)視頻清晰度的字符串返回相應(yīng)的文本資源 ID。ximalayaDownloadIcon: 根據(jù)喜馬拉雅媒體項的下載類型返回相應(yīng)的下載圖標(biāo)資源 ID。
- 多源媒體適配: 提供了針對不同媒體源(酷我、喜馬拉雅、樂聽)的資源轉(zhuǎn)換方法。例如,
playingCoverPlaceHolder會根據(jù)mediaId判斷媒體源,并返回相應(yīng)的占位圖。 - 文案和狀態(tài)轉(zhuǎn)換: 將邏輯狀態(tài)(如下載狀態(tài)、二維碼狀態(tài))轉(zhuǎn)換為用戶可讀的文本資源 ID。例如,
downloadDisabledReason根據(jù)下載狀態(tài)返回相應(yīng)的禁用原因文本。
DataBindingConvertor.java
DataBindingConvertor 是一個簡單的 Java 類,它使用 @BindingConversion 注解定義了一些類型轉(zhuǎn)換方法。它的核心作用是:
- 自動類型轉(zhuǎn)換: 實現(xiàn)了 Android Data Binding 框架中的自動類型轉(zhuǎn)換功能。這使得開發(fā)者可以在 XML 布局文件中直接使用原始類型(如
int、CharSequence、Drawable、Uri),而無需手動將其包裝成TextResource或ImageResource。 - 簡化 XML 綁定: 例如,當(dāng)你在 XML 中將一個
int類型的資源 ID 綁定給一個需要TextResource的屬性時,convertTextResource(int id)方法會自動被調(diào)用,完成類型轉(zhuǎn)換。 - 處理特定數(shù)據(jù)模型: 提供了針對
QrCodeTokenInfo、OrderBean和IqiyiOrderBean的轉(zhuǎn)換方法,用于從這些數(shù)據(jù)模型中提取二維碼 URL。
DataBindingNavigationUtil.kt
DataBindingNavigationUtil 是一個工具類,專門用于處理與導(dǎo)航相關(guān)的邏輯。它的核心作用是:
- Fragment 和 Activity 導(dǎo)航: 提供了
openFragment等方法,用于在不同的上下文(Activity或Fragment)中打開新的 Fragment。 - 業(yè)務(wù)邏輯封裝: 封裝了與具體業(yè)務(wù)相關(guān)的導(dǎo)航邏輯,例如:
kuwoVipPayFragment,ximalayaLoginFragment: 創(chuàng)建并返回特定媒體源的 VIP 支付或登錄 Fragment。buildXimalayaPayDialog: 根據(jù)喜馬拉雅媒體項的購買類型,構(gòu)建并返回一個定制的購買彈窗對話框。ximalayaBuyFragment: 根據(jù)登錄狀態(tài)和媒體項的購買類型,決定并返回正確的 Fragment(登錄、VIP 購買或?qū)]嬞徺I)。
- 播放控制和彈窗: 提供了處理播放相關(guān)彈窗和 Fragment 的方法,例如
showRecognizeDialog用于顯示正在識別歌曲的彈窗,switchLrcFragment用于切換歌詞視圖。 - 上下文處理: 包含了一些輔助方法,用于在
FragmentContextWrapper中獲取實際的Fragment對象,以及處理上下文的層級關(guān)系。這使得導(dǎo)航工具類可以在不同類型的上下文中通用。
提供的這五個類是為了實現(xiàn)一套強大、靈活且易于維護的數(shù)據(jù)綁定和UI構(gòu)建框架,尤其是在處理多媒體應(yīng)用中常見的復(fù)雜UI和動態(tài)數(shù)據(jù)時。這種設(shè)計模式遵循了許多軟件工程的最佳實踐,例如關(guān)注點分離、模塊化和代碼復(fù)用。
以下是詳細(xì)解釋為什么要這樣設(shè)計的理由:
1. 關(guān)注點分離 (Separation of Concerns)
DataBindingConvertor.java和BindingResourceUtil.kt: 這兩個類負(fù)責(zé)將后端數(shù)據(jù)模型(如MediaMetadataCompat、QrCodeTokenInfo)轉(zhuǎn)換為UI所需的資源類型(如TextResource、ImageResource)或特定的文本格式(如播放速度字符串、二維碼URL)。它們將數(shù)據(jù)處理和格式化的邏輯從視圖層(XML布局)和視圖模型層中分離出來,使得數(shù)據(jù)模型可以保持簡潔,而不必包含與UI呈現(xiàn)相關(guān)的復(fù)雜邏輯。DatabindingAdapter.kt: 這個類專門負(fù)責(zé)將數(shù)據(jù)綁定到實際的UI組件上。它處理諸如圖片加載(使用Glide)、設(shè)置圓角、動態(tài)調(diào)整布局、添加動畫等所有與視圖操作相關(guān)的細(xì)節(jié)。這使得XML布局文件只專注于聲明性的UI結(jié)構(gòu),而不需要包含任何實現(xiàn)細(xì)節(jié)。OutlineProvider.kt: 這個類將View的輪廓裁剪邏輯獨立出來,專門用于為View設(shè)置不同類型的圓角。這使得圓角效果可以被復(fù)用,并且與View的背景、內(nèi)容等其他屬性完全解耦。
2. 代碼復(fù)用和模塊化
DataBindingConvertor.java和BindingResourceUtil.kt: 許多UI元素(如播放速度圖標(biāo)、下載按鈕圖標(biāo))在應(yīng)用的多個地方都會用到。將這些轉(zhuǎn)換邏輯集中在這兩個工具類中,可以避免在每個地方都重復(fù)編寫相同的when或if-else語句來判斷狀態(tài)和選擇資源。DatabindingAdapter.kt: 通過創(chuàng)建自定義的BindingAdapter,可以在多個XML布局文件中復(fù)用相同的UI綁定邏輯。例如,setImageUrlWithRadius方法可以用于應(yīng)用中任何需要加載帶圓角圖片的ImageView,而無需在每個布局或Fragment中手動調(diào)用 Glide。OutlineProvider.kt: 同樣的,如果多個 View 需要相同的圓角效果(例如,所有專輯封面都需要四個角的圓角),只需在 XML 中通過數(shù)據(jù)綁定設(shè)置OutlineProvider即可,無需在每個地方都創(chuàng)建新的ViewOutlineProvider對象。
3. 易于維護和擴展
- 清晰的職責(zé): 當(dāng)需要修改某個功能時,開發(fā)者可以快速定位到相應(yīng)的類。例如,要改變所有播放速度圖標(biāo)的樣式,只需修改
BindingResourceUtil.kt中的playbackSpeedIcon方法即可,而無需在多個文件中尋找和修改代碼。要調(diào)整圖片加載邏輯,只需修改DatabindingAdapter.kt中的 Glide 相關(guān)方法。 - 靈活的導(dǎo)航:
DataBindingNavigationUtil.kt將所有復(fù)雜的導(dǎo)航邏輯(例如,根據(jù)媒體項類型決定打開哪個支付或登錄Fragment)封裝在一個地方。這使得在需要修改導(dǎo)航流程時,開發(fā)者只需關(guān)注這個工具類,而不是在不同的Activity或Fragment中修改分散的startActivity或beginTransaction代碼。 - 適應(yīng)多媒體源: 媒體應(yīng)用通常需要支持多種媒體源(如酷我、喜馬拉雅、愛奇藝)。
BindingResourceUtil.kt中的許多方法都通過判斷mediaId來適配不同的媒體源,并返回相應(yīng)的資源,這使得應(yīng)用能夠輕松地處理多媒體源帶來的差異性。
到此這篇關(guān)于Android 自定義Binding Adapter實戰(zhàn)應(yīng)用的文章就介紹到這了,更多相關(guān)Android 自定義Binding Adapter內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android使用CardView作為RecyclerView的Item并實現(xiàn)拖拽和左滑刪除
這篇文章主要介紹了Android使用CardView作為RecyclerView的Item并實現(xiàn)拖拽和左滑刪除,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11
Android 異步獲取網(wǎng)絡(luò)圖片并處理導(dǎo)致內(nèi)存溢出問題解決方法
Android異步獲取網(wǎng)絡(luò)圖片并處理圖片Out Of Memory內(nèi)存溢出如何解決呢?本文介紹了操作步驟,感興趣的朋友可以了解下或許對你有所幫助2013-02-02
舉例講解Android應(yīng)用中SimpleAdapter簡單適配器的使用
這篇文章主要介紹了Android應(yīng)用中SimpleAdapter簡單適配器的使用例子,SimpleAdapter經(jīng)常在ListView被使用,需要的朋友可以參考下2016-04-04
Android RecycleView 實現(xiàn)左滑上下分層示例代碼(自定義功能)
這篇文章主要介紹了Android RecycleView 實現(xiàn)左滑上下分層示例代碼(自定義功能),具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03
Rocksdb?Memtable數(shù)據(jù)結(jié)構(gòu)源碼解析
這篇文章主要為大家介紹了Rocksdb?Memtable數(shù)據(jù)結(jié)構(gòu)源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11
Android Studio報錯unable to access android sdk add-on list解決方案
這篇文章主要介紹了Android Studio報錯unable to access android sdk add-on list解決方案,本文通過多種方式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03

