我之前做過一個封裝,也發(fā)過一篇文章,http://m.itdecent.cn/p/7e81f4f02a2c
但是在我之后的開發(fā)時我又不斷的去為這個封裝組件添加新東西, 關(guān)鍵是添加多了就變得臃腫,再加上我當(dāng)時犯了一個很大的錯誤,我對這個組件進(jìn)行擴(kuò)展時我沒有寫注釋也沒有寫文檔,導(dǎo)致我現(xiàn)在每次看這個組件內(nèi)部的代碼都要研究一段時間,所以我決定防止之前的那個組件加上對面向?qū)ο筮M(jìn)一步的了解進(jìn)行重新的封裝。
不多說,先上gayhub地址,節(jié)約時間就隨便先寫了兩個demo
https://github.com/994866755/handsomeYe.KylinCheckListView
一.展示頁面
1.設(shè)計布局
首先做的是先設(shè)計我們要展示怎么樣子的頁面,按我的做法會先畫個簡單的圖。

還是分左右兩種情況,這樣我可以設(shè)計成Item使用LinearLayout來通過來按順序addView,這個順序可以定義一個showLacation參數(shù)來決定。
2.展示的思路
展示的思路我想按我之前封裝的那個一樣,我覺得那個思路很好。就是內(nèi)部封裝寫好adapter和viewholder,圖中內(nèi)部的方塊用個viewmodel來代替,這個viewmodel由外界傳入,就相當(dāng)于普通RecyclerView的viewholder
3.開發(fā)基本頁面
先不考慮單選/多選的邏輯操作,先不考慮與外部的關(guān)聯(lián),先不考慮組件的擴(kuò)展,而是現(xiàn)在組件內(nèi)部把頁面給跑起來。
public class KylinCheckListView extends FrameLayout{
// 單選與多選
public static final int RADIO = 0;
public static final int MULTISELECT = 1;
// 顯示左/顯示右
public static final int CHECKLEFT = 0;
public static final int CHECKRIGHT = 1;
// 列表控件
protected RecyclerView mRecyclerView;
// 顯示位置
protected int showLacation = CHECKLEFT;
// 子布局
protected Class<?> itemClass;
// 數(shù)據(jù)
protected List<String> datalist = new ArrayList<>();
// 適配器
protected KylinCheckListAdapter mAdapter;
public KylinCheckListView(Context context) {
super(context);
create();
}
public KylinCheckListView(Context context, AttributeSet attrs) {
super(context, attrs);
// todo 在xml中添加自定義參數(shù)
create();
}
public KylinCheckListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public KylinCheckListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* 初始化操作
*/
protected void create(){
initView();
initAdapter();
}
/**
* 初始化View
*/
protected void initView(){
mRecyclerView = initList();
this.addView(mRecyclerView);
}
/**
* 初始化RecyclerView
*/
protected RecyclerView initList(){
RecyclerView recyclerView = new RecyclerView(getContext());
FrameLayout.LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
recyclerView.setLayoutParams(lp);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));// todo 可設(shè)置傳入?yún)?shù) 布局
recyclerView.addItemDecoration(new BerItemDecoration(1)); // todo 可設(shè)置傳入?yún)?shù) 間距
return recyclerView;
}
/**
* 初始化Adapter
*/
protected void initAdapter(){
mAdapter = new KylinCheckListAdapter();
}
/**
* 設(shè)置數(shù)據(jù)
*/
public void setDataToView(List<String> datalist){
this.datalist = datalist;
mRecyclerView.setAdapter(mAdapter);
}
// todo 添加刪除列表數(shù)據(jù)等操作
/**
* get or set
*/
//返回列表
public RecyclerView getRecyclerView() {
return mRecyclerView;
}
// 設(shè)置Item的布局類
public void setItemClass(Class<?> itemClass) {
this.itemClass = itemClass;
}
/**
* RecycerView 的 Adapter
*/
public class KylinCheckListAdapter extends RecyclerView.Adapter<KylinCheckListViewHolder>{
@Override
public int getItemViewType(int position) {
return super.getItemViewType(position);
}
@Override
public KylinCheckListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LinearLayout itemLayout = initChildLayout();
KylinCheckListViewHolder viewHolder = new KylinCheckListViewHolder(itemLayout);
return viewHolder;
}
protected LinearLayout initChildLayout(){
LinearLayout itemLayout = new LinearLayout(getContext());
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// todo 可以在此設(shè)置padding等操作
itemLayout.setLayoutParams(lp);
itemLayout.setOrientation(LinearLayout.HORIZONTAL);
itemLayout.setBackgroundResource(R.color.app_blue);
return itemLayout;
}
@Override
public void onBindViewHolder(KylinCheckListViewHolder holder, int position) {
holder.setPosition(position);
holder.setData(datalist.get(position));
}
@Override
public int getItemCount() {
return datalist.size();
}
}
/**
* RecycerView 的 ViewHolder
*/
public class KylinCheckListViewHolder extends RecyclerView.ViewHolder{
protected CheckBox mCheckBox;
protected String data;
protected int position;
protected CheckViewModel mViewModel;
public KylinCheckListViewHolder(View itemView) {
super(itemView);
initView();
}
private void initView(){
mCheckBox = initCheckBox();
mViewModel = initViewModel();
if (itemView instanceof LinearLayout){
LinearLayout llContent = (LinearLayout) itemView;
// 沒有核心布局的話沒必要線束布局
if (mViewModel != null){
View contentChild = mViewModel.getContentView();
ViewGroup.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
contentChild.setLayoutParams(lp);
if (showLacation == CHECKLEFT){
llContent.addView(mCheckBox);
llContent.addView(contentChild);
}else if (showLacation == CHECKRIGHT){
llContent.addView(contentChild);
llContent.addView(mCheckBox);
}
}
}
}
private CheckBox initCheckBox(){
CheckBox checkBox = new CheckBox(getContext());
LinearLayout.LayoutParams cbLp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
checkBox.setLayoutParams(cbLp);
return checkBox;
}
private CheckViewModel initViewModel(){
try {
// todo 添加Bundle情況的反射
Class[] paramTypes = new Class[]{Context.class};
Object[] params = new Object[]{getContext()};
Constructor con = itemClass.getConstructor(paramTypes);
mViewModel = (CheckViewModel) con.newInstance(params);
return mViewModel;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
public void setData(String data){
this.data = data;
if (mViewModel != null){
mViewModel.setData(data);
}
}
public void setPosition(int position) {
this.position = position;
}
}
}
按照這樣寫法(當(dāng)然寫代碼的中間調(diào)整了很多細(xì)節(jié)),可以得到結(jié)果

這樣確實(shí)能得到一個我想要的結(jié)果,默認(rèn)情況下選框在左,核心布局由外部傳入通過反射來實(shí)現(xiàn)。
看看代碼中的一些細(xì)節(jié)
(1)內(nèi)部類沒有使用靜態(tài)內(nèi)部類是因?yàn)槲蚁胱寖?nèi)部類直接能拿到外部類的變量,如果用靜態(tài)內(nèi)部類的話要定義然后傳進(jìn)去,參數(shù)有點(diǎn)多,我不想這樣做。
(2)核心布局是由外部傳入一個Class類讓進(jìn)行反射得到對象,我以前是寫通過類名得到,但是傳類名的話要把包名也寫上,太啰嗦了,這里就改成傳class對象。
其它也沒什么,到這步代碼也不難看懂。
二.添加數(shù)據(jù)
1.設(shè)計數(shù)據(jù)結(jié)構(gòu)
頁面能正常展示之后可以開始設(shè)計數(shù)據(jù)結(jié)構(gòu)了。我打算把數(shù)據(jù)結(jié)構(gòu)設(shè)計成像之前封裝的那樣,就是先寫個基類結(jié)構(gòu)來保存選框的選擇狀態(tài)。
public class CheckListEntity {
public boolean isCheck = false;
}
然后在之前的代碼中用泛型來代替寫死的String類型
public class KylinCheckListView<T extends CheckListEntity> extends FrameLayout{
傳進(jìn)這個組件的列表的數(shù)據(jù)類型強(qiáng)制要求繼承CheckListEntity類
2.isCheck 關(guān)聯(lián)選框
更改viewholder中的seyData()方法
public void setData(T data){
this.data = data;
// 設(shè)置Item數(shù)據(jù)
if (mViewModel != null){
mViewModel.setData(data);
}
// 設(shè)置選框狀態(tài)
if (mCheckBox != null){
mCheckBox.setChecked(data.isCheck);
}
}
更改viewholder中的initCheckBox()方法,添加點(diǎn)擊選框的事件
private CheckBox initCheckBox(){
CheckBox checkBox = new CheckBox(getContext());
LinearLayout.LayoutParams cbLp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
checkBox.setLayoutParams(cbLp);
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
data.isCheck = isChecked;
// todo 單選情況下的邏輯
}
});
return checkBox;
}
這樣就能正常的關(guān)聯(lián)view和data。但是我想再多說個我開發(fā)過程中的心得。
怎么知道數(shù)據(jù)已經(jīng)關(guān)聯(lián)?我個人是使用Debug來觀察數(shù)據(jù)的走向,所以我想說的是,像我開發(fā)這個組件,功能很多,代碼最后算起來也有幾百上千行,這種情況下首先你要明確你一個開發(fā)的流程,想好再動手,然后這種情況肯定大量的用到Debug,所以不熟悉的肯定要弄懂,最后就是todo注釋,可以看到我代碼中有些地方先寫了todo
三.選框的邏輯操作
數(shù)據(jù)已經(jīng)不用關(guān)心了,我們把數(shù)據(jù)丟給相對的CheckViewModel來做,之后怎么處理展示頁面中Item的數(shù)據(jù),是CheckViewModel的事,而CheckListView內(nèi)部也已經(jīng)通過isChecked拿到它只關(guān)心的選框展示,所以已經(jīng)拿到數(shù)據(jù)后開始做選框的邏輯操作。
1.單選情況
單選情況需要點(diǎn)擊另一個選框,之前的選框選項就會被取消。
我之前有個加了todo的地方,可以看到我現(xiàn)在就能很快的找到在哪塊地方寫這個單選的邏輯。
選著優(yōu)秀的算法來開發(fā)邏輯
要做這個邏輯,可能很多人會想就是,點(diǎn)擊之后,用個for循環(huán),把選中的position的data的isChecked變成true,然后其它變成false,再刷新列表。是不是,肯定很多人會有這種想法。雖然這種做法也行,而且我覺得對性能基本沒影響,但是我有強(qiáng)迫癥,我看到if啊,看到for啊,我就渾身不舒服。所以我打算用空間換取時間的做法:
定義一個變量來保存正在選中的選框的position,之后的做法就不用我說了吧。
在事件中設(shè)置邏輯
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (showType == RADIO){
// 單選情況
Observable.just(position).subscribe(radioCheckAct);
}else {
//多選情況
data.isCheck = isChecked;
//todo 多選情況下的其它操作
}
}
});
用觀察者
private Action1<Integer> radioCheckAct = new Action1<Integer>() {
@Override
public void call(Integer integer) {
// 判斷邊界
if (integer >= datalist.size()){
return;
}
// 重復(fù)選的話別浪費(fèi)時間去刷新
if (integer == radioIndex){
return;
}
if (radioIndex != -1){
datalist.get(radioIndex).isCheck = false;
}
datalist.get(integer).isCheck = true;
radioIndex = integer;
updataAdapter();
}
};
private void updataAdapter(){
mAdapter.notifyDataSetChanged();
}
運(yùn)行后發(fā)現(xiàn)報錯Cannot call this method while RecyclerView is computing a layout or scrolling。
讀不懂是什么意思,然后上網(wǎng)查,發(fā)現(xiàn)很多解決方法。這里再說說我的一個開發(fā)心得,防蟲和抓蟲。
比如說我在網(wǎng)上找這個BUG,很多文章都是告訴你怎么怎么解決啊,直接貼代碼給你啊,其實(shí)我很不喜歡這種文章,我就真的是很坦白說了,我不喜歡這種文章。
當(dāng)我還是一個萌新時,我會直接抄解決的代碼下來,正常就不管,還是不正常就找其他方法。這其實(shí)是一個防蟲的過程,就算成功運(yùn)行,你也只是防止了這個BUG發(fā)生,但是它可以通過其它環(huán)境或者方式再次使你的程序出BUG,這是一種不安全的方式,所以我很不建議就是直接抄別人的代碼。你必須要找到這個錯誤在哪,這就是抓蟲,就算你花很多時間你也要去抓。
其實(shí)道理很簡單,你發(fā)現(xiàn)一只蚊子,你給自己噴花露水(防蟲),還是拍死它(抓蟲)。
回來講講這個BUG,我找了很多文章都沒有詳細(xì)說這個BUG是什么情況,我只能慢慢Debug去找,發(fā)現(xiàn)了最終的問題。
如果我調(diào)用 notifyDataSetChanged()的話,會更新列表,但是列表設(shè)置數(shù)據(jù)時執(zhí)行了mCheckBox.setChecked(data.isCheck); 而狀態(tài)一變就會去調(diào)用那個監(jiān)聽的方法,就會notifyDataSetChanged(),也就是說在notifyDataSetChanged()的過程中執(zhí)行notifyDataSetChanged(),所以就會報這個錯誤。
那我就不應(yīng)該用setOnCheckedChangeListener監(jiān)聽,改用setOnClickListener來監(jiān)聽
checkBox.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (showType == RADIO){
// 單選情況
Observable.just(position).subscribe(radioCheckAct);
}else {
//todo 多選情況下的其它操作
}
}
});
這樣就能正常實(shí)現(xiàn)效果

2.多選情況
和單選一樣,也是使用Rx來做
// 多選情況下的觀察者
private Action1<Integer> multCheckAct = new Action1<Integer>() {
@Override
public void call(Integer integer) {
// 判斷邊界
if (integer >= datalist.size()){
return;
}
datalist.get(integer).isCheck = !(datalist.get(integer).isCheck);
updataAdapter(); // todo 優(yōu)化成局部刷新
}
};
3.返回選擇結(jié)果
既然是封裝,必然需要返回所選取的選項的結(jié)果。
這里我打算先定義一個接口來定義這個View的和外界對接的行為。
public interface KylinCheckListImpl {
/**
* 單選的結(jié)果
*/
int radioResult();
/**
* 多選的結(jié)果
*/
List<Integer> multResults();
/**
* 多選時已選擇的數(shù)量 (可以用這個數(shù)量來判斷是否全選)
*/
int multCount();
/**
* 全選/全不選
*/
void multAllCheck(boolean state);
/**
* 單選
*/
void radioCheck(int position);
/**
* 設(shè)置選擇
*/
void check(int position);
/**
* 懶更新
*/
void lazyUpdate();
}
暫時先就想到這么多View和外界交互的行為,讓KylinCheckListView實(shí)現(xiàn)這個就接口,然后重寫接口中的方法。
上面定義的radioCheck(int position)和check(int position)區(qū)別在于一個馬上更新,一個不會馬上更新
還有寫這個接口有什么用,如果你有新的交互方法要擴(kuò)展,直接在接口里面定義。
4.寫接口中的方法
(1)radioResult
public int radioResult() {
return radioIndex;
}
之前我說過radioIndex是記錄單選的坐標(biāo),所以這里直接返回這個就是單選的結(jié)果,-1代碼沒選。
(2)multResults
返回多選的結(jié)果比較麻煩,我需要返回一個數(shù)組
public List<Integer> multResults() {
List<Integer> resultList = new ArrayList<>();
for (int i = 0; i < datalist.size(); i++) {
if (datalist.get(i).isCheck) {
resultList.add(i);
}
}
return resultList;
}
(3)multCount
public int multCount() {
int count = 0;
for (int i = 0; i < datalist.size(); i++) {
if (datalist.get(i).isCheck) {
count++;
}
}
return count;
}
一般我們可以拿到這個多選時的數(shù)量和總數(shù)量對比,判斷是否全選,但是想想,如果我要做一個每選擇一次都要判斷是否全選的話,就會沒選一次都進(jìn)行一次循環(huán),這雖然對體驗(yàn)沒什么影響,但我有強(qiáng)迫癥,我肯定不容許每次都浪費(fèi)時間去重復(fù)循環(huán),所以我想了一個新的辦法來判斷是否全選。
(4)isMultAll
新添加的方法,用于判斷是否全選
public int isMultAll(int count,int position){
if (count < 1){
return 0;
}
if (position >= datalist.size()){
return count;
}
if (datalist.get(position).isCheck){
return count++;
}else {
return count--;
}
}
可以看到我傳入兩個參數(shù),第一個表示目前總選擇的數(shù)量,第二個表示當(dāng)前點(diǎn)擊的選項的下標(biāo)。
我是這樣想的,如果做每點(diǎn)一次都判斷多選的話,我可以在一開始先用multCount獲取到總選中的數(shù)量,然后再對每次選中的進(jìn)行判斷,如果當(dāng)次選中的選項的isCheck是true,那就加1,相反減1,然后返回,這樣就不用每次都做循環(huán)。至于行不行,等我全部寫完試試就知道了。
(5)multAllCheck
根據(jù)傳入的boolean值來設(shè)置全選或者全不選
public void multAllCheck(boolean state) {
for (int i = 0; i < datalist.size(); i++) {
datalist.get(i).isCheck = state;
}
mAdapter.notifyDataSetChanged();
}
(6)radioCheck
public void radioCheck(int position) {
if (position < datalist.size()){
datalist.get(position).isCheck = true;
mAdapter.notifyDataSetChanged();
}
}
(7)check 和 lazyUpdate
這兩個就是把radioCheck的設(shè)置和更新給分開
好了,這樣就把一些和外界常用的交互給做完了,現(xiàn)在試試效果。是我直接用debug來試
單選radioCheck,出了點(diǎn)問題,改一下
public void radioCheck(int position) {
Observable.just(position).subscribe(radioCheckAct);
}
然后發(fā)現(xiàn)多選的isMultAll還是出問題,后來想想這種做法也不對,違反了迪米特原則,改一下。
我把count定義在內(nèi)部,由我來做操作,而不是將它暴露給用戶去使用。
public boolean isMultAll(){
return multCount == datalist.size();
}

到這里就做到了一些常用的與外界的關(guān)聯(lián)邏輯
四.設(shè)置事件
我這暫時只用到了選中時的事件,之后擴(kuò)展也不難,所以這里就先寫選中的事件。
public interface KylinOnCheckListener {
void kylinCheckChange(int position,boolean isChecked);
}
兩個觀察者內(nèi)部加監(jiān)聽的操作
// 單選情況下的觀察者
private Action1<Integer> radioCheckAct = new Action1<Integer>() {
@Override
public void call(Integer integer) {
// 判斷邊界
if (integer >= datalist.size()){
return;
}
// 重復(fù)選的話別浪費(fèi)時間去刷新
if (integer == radioIndex){
return;
}
if (radioIndex != -1){
datalist.get(radioIndex).isCheck = false;
}
datalist.get(integer).isCheck = true;
radioIndex = integer;
updataAdapter();
if (kylinOnCheckListener != null){
kylinOnCheckListener.kylinCheckChange(radioIndex,true);
}
}
};
// 多選情況下的觀察者
private Action1<Integer> multCheckAct = new Action1<Integer>() {
@Override
public void call(Integer integer) {
// 判斷邊界
if (integer >= datalist.size()){
return;
}
datalist.get(integer).isCheck = !(datalist.get(integer).isCheck);
if (datalist.get(integer).isCheck){
multCount++;
}else {
multCount--;
}
updataAdapter(); // todo 優(yōu)化成局部刷新
if (kylinOnCheckListener != null){
kylinOnCheckListener.kylinCheckChange(radioIndex,datalist.get(integer).isCheck);
}
}
};
五.View屬性擴(kuò)展
之前做了那個搜索框組件發(fā)現(xiàn)用attrs的話那個尺寸有問題,我這里打算測試一下自定義view中的getDimension得到的結(jié)果是什么
首先結(jié)果肯定是一個float類型的沒錯
(1)直接寫默認(rèn)值不加單位
type.getDimension(R.styleable.KylinCheckListViewStyle_cb_margin_left,16);
得到結(jié)果: 16.0
(2)傳dp
type.getDimension(R.styleable.KylinCheckListViewStyle_cb_margin_left, DimensionUtils.dip2px(getContext(),16));
得到結(jié)果: 48.0
DimensionUtils.dip2px(getContext(),16)的結(jié)果: 48.0
(3)在xml中傳px
app:cb_margin_left = "16px"
得到結(jié)果: 16.0
(4)在xml中傳dp
app:cb_margin_left = "16dp"
得到結(jié)果:48.0
看了下結(jié)果,沒毛病啊,但是為什么我之前那個搜索的就這么怪,先不管了,這里沒毛病的話先直接弄。
添加常用的屬性
<!-- 選框樣式 -->
<declare-styleable name="KylinCheckListViewStyle">
<!-- checkbox的margin -->
<attr name="cb_margin_left" format="dimension"/>
<attr name="cb_margin_right" format="dimension"/>
<attr name="cb_margin_top" format="dimension"/>
<attr name="cb_margin_bottom" format="dimension"/>
<!-- checkbox的背景-->
<attr name="cb_backgroup" format="reference"/>
<!-- item的背景-->
<attr name="item_backgroup" format="reference"/>
<!-- item的gravity-->
<attr name="item_gravity" format="integer"/>
<!-- recyclerview的分割線-->
<attr name="item_decoration" format="dimension"/>
</declare-styleable>
我目前就寫這么多,實(shí)在沒時間加太多。
六.添加數(shù)據(jù)的操作
發(fā)現(xiàn)還沒有做完啊,還要添加數(shù)據(jù)的增刪。
/**
* 添加數(shù)據(jù)
*/
public void addData(List<T> datas){
datalist.addAll(datas);
updataAdapter();
}
/**
* 添加數(shù)據(jù)
*/
public void addData(T data){
datalist.add(data);
updataAdapter();
}
/**
* 刪除
*/
public void removeData(int position){
datalist.remove(position);
updataAdapter();
}
把datalist設(shè)置個get方法,增強(qiáng)擴(kuò)展。
七.總結(jié)
全部代碼在gayhub,這里就不重復(fù)貼了,抓緊時間直接做個總結(jié)。
1.不要重復(fù)造輪子
我這里重寫封裝已經(jīng)封裝過的組件是一個錯誤的做法,主要以前做擴(kuò)展的時候沒有整理,導(dǎo)致多次擴(kuò)展后原來的組件變得有點(diǎn)亂,所以要講第二點(diǎn)
2.擴(kuò)展時要進(jìn)行整理和更新文檔
3.開發(fā)前先想清楚要做成什么樣子,想清楚開發(fā)的步驟
4.碰到BUG時,盡量抓蟲而不是防蟲
框架擴(kuò)展
在實(shí)際項目中碰到問題對框架進(jìn)行擴(kuò)展,但是這里寫得太多內(nèi)容,就分出去寫一篇新的文章來講所擴(kuò)展的內(nèi)容。