Android練手小項目(KTReader)基于mvp架構(gòu)(四)

上路傳送眼:

Android練手小項目(KTReader)基于mvp架構(gòu)(三)

GIthub地址: https://github.com/yiuhet/KTReader

上篇文章中我們完成了知乎日報詳情頁煮甥。
而這次我們要做的的就是完成整個豆瓣模塊(這篇文章有點長风皿,請先馬后看)涩赢。

效果圖獻上:


效果圖

從效果圖中可以看到莹妒,這次我們加入的豆瓣模塊采用了常見的TabLayout+ViewPager實現(xiàn)界面的切換(此處有個坑,后文會提到),子fragment實現(xiàn)的功能如下:

  • 圖書:

圖書頁我們放置了一個SearchView肌毅,讓用戶輸入圖書名后獲得返回結(jié)果秒梳,點擊進入詳情頁梆砸。
用到的新東西:

SearchView
折疊textview+標(biāo)題布局(三種方法)

  • 電影:

電影頁我們放置了兩個HorizontalScrollView實現(xiàn)水平滾動分別顯示正在熱映的電影和豆瓣電影的top250(充滿惡意的想把top去掉)。點擊進入相應(yīng)的電影詳情頁佩抹。
用到的新東西:

HorizontalScrollView
RecyclerView.Adapter下多個ViewHolder

  • 音樂:

音樂頁我們放置了一個標(biāo)簽列表叼风,點擊相應(yīng)標(biāo)簽,呈現(xiàn)相應(yīng)音樂類型的結(jié)果棍苹,并沒有詳情頁(沒錯无宿,就是懶,之后應(yīng)該會補上枢里,我會說原本還有個同城頁直接被我刪了fragment么2333)孽鸡。
用到的新東西:

RecyclerView的GridLayout布局
標(biāo)簽的最簡單實現(xiàn)(一個button硬是說成新東西,也是沒誰)

OK,前面廢話了那么多坡垫,下面給出具體實現(xiàn)方法

首先梭灿,我們要先寫好抽屜菜單的布局,這里就不給布局代碼了冰悠,簡單提兩句堡妒,就是兩個group,第一個中有四個模塊(知乎溉卓,豆瓣皮迟,奇聞,壁紙)桑寨,第二個為個性化(足跡伏尼,收藏,設(shè)置尉尾,關(guān)于)爆阶。之后在MainActivity的onNavigationItemSelected方法中讓其各回各家。

目前的方法:

@Override
    public boolean onNavigationItemSelected(MenuItem item) {
        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        int id = item.getItemId();
        int groupId = item.getGroupId();
        if (groupId == R.id.nav_group_fragment) {
            fragmentTransaction.replace(R.id.fragment_main, FragmentFactory.getInstance().getFragment(id)).commit();
        }
        switch (id) {
            case R.id.nav_zhihu :
                mToolbar.setTitle(R.string.title_zhihu);
                break;
            case R.id.nav_douban :
                mToolbar.setTitle(R.string.title_douban);
                break;
            case R.id.nav_qiwen :
                mToolbar.setTitle(R.string.title_qiwen);
                break;
            case R.id.nav_tupian :
                mToolbar.setTitle(R.string.title_tupian);
                break;
            case R.id.nav_history :
                break;
            case R.id.nav_save :
                break;
            case R.id.nav_setting :
                break;
            case R.id.nav_about :
                break;
            default:
                break;
        }
        mDrawerLayout.closeDrawer(GravityCompat.START);
        return true;
    }

其中的FragmentFactory代碼如下:

public class FragmentFactory {
    private static FragmentFactory sFragmentFactory;

    private BaseFragment mZHihuFragment;
    private Fragment mDoubanFragment;
    private BaseFragment mQiwenFragment;
    private BaseFragment mTupianFragment;

    public static FragmentFactory getInstance() {
        if (sFragmentFactory == null) {
            synchronized (FragmentFactory.class) {
                if (sFragmentFactory == null) {
                    sFragmentFactory = new FragmentFactory();
                }
            }
        }
        return sFragmentFactory;
    }

    public Fragment getFragment(int id) {
        switch (id) {
            case R.id.nav_zhihu:
                return getZHihuFragment();
            case R.id.nav_douban:
                return getDoubanFragment();
            case R.id.nav_qiwen:
                return getQiwenFragment();
            case R.id.nav_tupian:
                return getTupianFragment();
        }
        return null;
    }
    private BaseFragment getZHihuFragment() {
        if (mZHihuFragment == null) {
            mZHihuFragment = new ZhiHuFragment();
        }
        return mZHihuFragment;
    }
    private Fragment getDoubanFragment() {
        if (mDoubanFragment == null) {
            mDoubanFragment = new DoubanFragment();
            Log.d("ppapp","new DoubanFragment()");
        }
        Log.d("ppapp","getDoubanFragment");
        return mDoubanFragment;
    }
    private BaseFragment getQiwenFragment() {
        if (mQiwenFragment == null) {
            mQiwenFragment = new ZhiHuFragment();
        }
        return mQiwenFragment;
    }
    private BaseFragment getTupianFragment() {
        if (mTupianFragment == null) {
            mTupianFragment = new ZhiHuFragment();
        }
        return mTupianFragment;
    }
}

豆瓣模塊的網(wǎng)絡(luò)請求api
api.DoubanApi

public interface DoubanApi {
    /**
     * 圖書Api
     */
    @GET("book/{text}")
    Observable<DoubanBookDetail> getSearchBookDetail(@Path("text")  String text,@Query("start") String start); //搜索圖書
    @GET("book/search")
    Observable<DoubanBook> getSearchBookByName(@Query("q")  String text, @Query("count") String count); //搜索圖書by關(guān)鍵字
    @GET("book/search")
    Observable<String> getSearchBookByTag(@Query("tag") String text); //搜索圖書by類型

    /**
     * 電影Api
     */
    @GET("movie/search")
    Observable<String> getSearchMovie(@Path("q") String text);//獲取電影條目搜索結(jié)果數(shù)據(jù)
    @GET("movie/in_theaters")
    Observable<DoubanMovieDetail> getInTheaters();//獲取熱映電影數(shù)據(jù)
    @GET("movie/coming_soon")
    Observable<String> getComingSonn();//獲取即將即將上映電影數(shù)據(jù)
    @GET("movie/top250")
    Observable<DoubanMovieDetail> getTop250(@Query("start") String start);//獲取Top250數(shù)據(jù)
    @GET("movie/weekly")
    Observable<String> getWeekly();//獲取口碑榜數(shù)據(jù)
    @GET("movie/new_movies")
    Observable<String> getNewMovies();//獲取新片榜數(shù)據(jù)
    @GET("movie/subject/{text}")
    Observable<DoubanMovieSubject> getMovieSubject(@Path("text")  String text);//獲取電影條目信息

    /**
     * 音樂Api
     */
    @GET("music/search")
    Observable<DoubanMusic> getSearchMusicByTag(@Query("tag")  String text, @Query("count") String count); //搜索音樂by關(guān)鍵字
    @GET("music/{id}")
    Observable<DoubanMusic> getSearchMusicById(@Path("text")  String id); //搜索音樂
}

RetrofitManager中需要添加新的獲取服務(wù)方法:
utils.RetrofitManager:

public DoubanApi getDoubanService(String url) {
        if (doubanApi == null) {
            doubanApi = new Retrofit.Builder()
                    .baseUrl(url) //必須以‘/’結(jié)尾
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//使用RxJava2作為CallAdapter
                    .client(client)//如果沒有添加,那么retrofit2會自動給我們添加了一個。
                    .addConverterFactory(GsonConverterFactory.create())//Retrofit2可以幫我們自動解析返回數(shù)據(jù)辨图,
                    .build().create(DoubanApi.class);
        }
        return doubanApi;
    }

準(zhǔn)備工作做完后班套,
下面開始進行DoubanFragment的實現(xiàn)
這里先講講所遇見的坑:

fragment+tablayout+viewpager+多個fragment內(nèi)容不顯示

具體表現(xiàn)就是點擊豆瓣精選,后點擊知乎專欄故河,再點擊回豆瓣精選吱韭,發(fā)現(xiàn)豆瓣頁的內(nèi)容神奇的被整沒了。
究其原因是由于嵌套fragment產(chǎn)生了這個bug鱼的,當(dāng)寄生fragment銷毀視圖時理盆,其內(nèi)部的子fragment并沒有銷毀宙橱,然后再次生成這個fragment時积担,其子fragment的視圖不會重建的烁。
查了好久資料襟雷,都與自己的情況不符(用的support v4包),最后換個方式直接尋找銷毀子fragment的方法砰诵,最終解決了表面的問題偿乖,解決方案在DoubanFragment的removeChildFragment方法里竣稽。

ps:崩潰的是解決了這個,然后詳情頁點擊返回按鈕時應(yīng)用又崩了变汪,
機智的我就把返回按鈕給變沒了,反正可以右滑返回不是么(滑稽臉)涮瞻。
不過偷懶不是好孩子窒升,以后還要回來根除這個bug酪呻,尋找更好的解決方案闷尿。

ui.fragment.douban.DoubanFragment:

public class DoubanFragment extends Fragment {

    @BindView(R.id.tablay_douban)
    TabLayout mTablayDouban;
    @BindView(R.id.vp_douban)
    ViewPager mVpDouban;
    Unbinder unbinder;

    DoubanAdapter doubanAdapter ;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    /**
     * 坑 fragment+tablayout+viewpager+多個fragment內(nèi)容不顯示
     * http://www.cnblogs.com/mengdd/p/5552721.html
     * @param inflater
     * @param container
     * @param savedInstanceState
     * @return
     */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_douban, container, false);
        unbinder = ButterKnife.bind(this, view);
        doubanAdapter = new DoubanAdapter(getActivity().getSupportFragmentManager());
        mVpDouban.setAdapter(doubanAdapter);
        mTablayDouban.setupWithViewPager(mVpDouban);
        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d("pppppa","onDestroyView");
        removeChildFragment();
        unbinder.unbind();
    }

    public void removeChildFragment() {
        FragmentManager fragmentManager = getFragmentManager();
        List<Fragment> fragmentList = fragmentManager.getFragments();
        for (int i =0;i<fragmentList.size(); i++){
            if (fragmentList.get(i) instanceof DoubanBookFragment
                    ||fragmentList.get(i) instanceof DoubanMovieFragment
                    ||fragmentList.get(i) instanceof DoubanMusicFragment){
                fragmentManager.beginTransaction()
                        .remove(fragmentList.get(i))
                        .commit();
            }
        }
    }
}

可以從代碼里看到TabLayout+ViewPager實現(xiàn)了滑動更改頁面的功能
下面給出適配器:
addpter.DoubanAdapter:

public class DoubanAdapter extends FragmentPagerAdapter {

    private final String[] mTitles = new String[]{
            "圖書", "電影", "音樂"};

    public DoubanAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        if (position == 1) {
            return new DoubanMovieFragment();
        } else if (position == 2) {
            return new DoubanMusicFragment();
        }
        return new DoubanBookFragment();
    }

    @Override
    public int getCount() {
        return mTitles.length;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mTitles[position];
    }

}

寄主fragment寫好之后就要寫各個頁面的具體實現(xiàn)了:

1.DoubanBookFragment

Model層

  • 模型實體類DoubanBook慣例直接GsonFormat工具生成
    (model.entity.DoubanBook)

  • 豆瓣圖書Model接口
    model.ZhihuDetailModel:

public interface DoubanBookModel {

    void loadSearch(String id, OnDoubanBookListener listener);//圖書搜索
}
  • 豆瓣圖書Model具體實現(xiàn)類
    model.imp1.DoubanBookModelImp1
public class DoubanBookModelImp1 implements DoubanBookModel {

    private DoubanApi mDoubanApiService; //請求服務(wù)

    public DoubanBookModelImp1() {
        mDoubanApiService = RetrofitManager
                .getInstence()
                .getDoubanService(Constant.DOUBAN_BASE_URL); //創(chuàng)建請求服務(wù)
    }
    @Override
    public void loadSearch(String id, final OnDoubanBookListener listener) {
        if (mDoubanApiService != null) {
            mDoubanApiService.getSearchBookByName(id,"50")
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Observer<DoubanBook>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull DoubanBook doubanBook) {
                            listener.onLoadSearchSuccess(doubanBook);
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            listener.onLoadDataError(e.toString());
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        }
    }
}

View層

  • 回調(diào)接口
    view.DoubanBookView
public interface DoubanBookView {
    void onStartGetData();

    void onGetSearchSuccess(DoubanBook doubanBook);

    void onGetDataFailed(String error);
}
  • 創(chuàng)建DoubanBookFragment
public class DoubanBookFragment extends BaseFragment<DoubanBookView, DoubanBookPresenterImp1> implements DoubanBookView {

    Unbinder unbinder;
    @BindView(R.id.searchView)
    SearchView mSearchView;
    @BindView(R.id.tv_count)
    TextView mTvCount;
    @BindView(R.id.recycleview_douban)
    RecyclerView mRecycleviewDouban;
    @BindView(R.id.prograss)
    ProgressBar mPrograss;

    private DoubanBookAdapter mDoubanBookAdapter;


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = super.onCreateView(inflater, container, savedInstanceState);
        unbinder = ButterKnife.bind(this, rootView);
        init();
        return rootView;
    }

    private void init() {
        mSearchView.setIconifiedByDefault(false);
        mSearchView.setQueryHint("查找圖書");
        mSearchView.setOnQueryTextListener(searchViewListener);
        mRecycleviewDouban.setLayoutManager(new LinearLayoutManager(getContext()));
        mRecycleviewDouban.setHasFixedSize(true);
        mRecycleviewDouban.setItemAnimator(new DefaultItemAnimator());
        mDoubanBookAdapter = new DoubanBookAdapter(getContext());
        mDoubanBookAdapter.setOnItemClickListener(mOnItemClickListener);
        mRecycleviewDouban.setAdapter(mDoubanBookAdapter);
    }

    private DoubanBookAdapter.OnItemClickListener mOnItemClickListener = new DoubanBookAdapter.OnItemClickListener() {
        @Override
        public void onItemClick(String id) {
            Intent intent = new Intent(getContext(), DoubanBookDetailActivity.class);
            intent.putExtra("DOUBANBOOKID",String.valueOf(id));
            startActivity(intent);
        }
    };
    private SearchView.OnQueryTextListener searchViewListener = new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            mPresenter.getSearch(query);
            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {
            return false;
        }
    };

    @Override
    protected int getLayoutRes() {
        return R.layout.fragment_douban_book;
    }

    @Override
    protected DoubanBookPresenterImp1 createPresenter() {
        return new DoubanBookPresenterImp1(this);
    }

    @Override
    public void onStartGetData() {
        mPrograss.setVisibility(View.VISIBLE);
    }

    @Override
    public void onGetSearchSuccess(DoubanBook doubanBook) {
        mPrograss.setVisibility(View.GONE);
        mTvCount.setText(String.format("找到%s個相關(guān)結(jié)果",doubanBook.total));
        mDoubanBookAdapter.addData(doubanBook);
    }

    @Override
    public void onGetDataFailed(String error) {
        mPrograss.setVisibility(View.GONE);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        unbinder.unbind();
    }

}

adapter的代碼
adapter.DoubanBookAdapter

public class DoubanBookAdapter extends RecyclerView.Adapter<DoubanBookAdapter.DoubanBookViewHolder>  {

    private Context mContext;
    private DoubanBook mDoubanBook;
    private OnItemClickListener mItemClickListener;

    public DoubanBookAdapter(Context context) {
        mContext = context;
    }

    public void addData(DoubanBook doubanBook) {
        mDoubanBook = doubanBook;
        notifyDataSetChanged();
    }
    @Override
    public DoubanBookAdapter.DoubanBookViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        DoubanBookItem doubanBookItem = new DoubanBookItem(mContext);
        return new DoubanBookViewHolder(doubanBookItem);
    }

    @Override
    public void onBindViewHolder(DoubanBookViewHolder holder, int position) {
        final DoubanBook.BooksEntity doubanBookList = mDoubanBook.books.get(position);
        holder.doubanBookItem.bindView(doubanBookList);
        holder.doubanBookItem.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mItemClickListener != null) {
                    mItemClickListener.onItemClick(doubanBookList.id);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return mDoubanBook == null ? 0 : mDoubanBook.count;
    }

    public class DoubanBookViewHolder extends RecyclerView.ViewHolder {

        public DoubanBookItem doubanBookItem;
        public DoubanBookViewHolder(DoubanBookItem itemView) {
            super(itemView);
            doubanBookItem = itemView;
        }
    }

    public interface OnItemClickListener {
        void onItemClick(String id);
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        mItemClickListener = listener;
    }
}

我們在adapter中寫了個個回調(diào)接口,當(dāng)圖書某項被點擊時女坑,調(diào)用回調(diào)實現(xiàn)activity的跳轉(zhuǎn)悠砚,跳轉(zhuǎn)到圖書詳情頁。

  • Presenter層

  • 回調(diào)接口
    presenter.DoubanBookPresenter
public interface DoubanBookPresenter {
    void getSearch(String id); //book搜索
}

presenter.listener.OnDoubanBookListener

public interface OnDoubanBookListener {
    void onLoadSearchSuccess(DoubanBook doubanBook);//book搜索
    void onLoadDataError(String error);
}
  • Presenter的實現(xiàn)
    presenter.imp1.DoubanBookPresenterImp1
public class DoubanBookPresenterImp1 extends BasePresenter<DoubanBookView> implements DoubanBookPresenter,OnDoubanBookListener{
    private DoubanBookView mDoubanBookView;
    private DoubanBookModelImp1 mDoubanBookModelImp1;

    public DoubanBookPresenterImp1(DoubanBookView doubanBookView) {
        mDoubanBookView = doubanBookView;
        mDoubanBookModelImp1 = new DoubanBookModelImp1();
    }

    @Override
    public void getSearch(String id) {
        mDoubanBookView.onStartGetData();
        mDoubanBookModelImp1.loadSearch(id, this);
    }

    @Override
    public void onLoadSearchSuccess(DoubanBook doubanBook) {
        mDoubanBookView.onGetSearchSuccess(doubanBook);
    }

    @Override
    public void onLoadDataError(String error) {
        mDoubanBookView.onGetDataFailed(error);
    }
}

這里也同時給出詳情頁的代碼
(model層和presenter層我也就不寫了堂飞,因為和上面的DoubanBookFragment實現(xiàn)基本一致):
activity.DoubanBookDetailActivity

public class DoubanBookDetailActivity extends MVPBaseActivity<DoubanBookDetailView, DoubanBookDetailPresenterImp1> implements DoubanBookDetailView {
    //大大的PS:BindView代碼我刪去了 太長
    MoreTextView mAuthorSummary;
    private int maxDescripLine = 3; //TextView默認(rèn)最大展示行數(shù)
    private String mBookId;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SwipeBackHelper.onCreate(this);
        ButterKnife.bind(this);
        initToolbar();
        initView();
    }

    private void initView() {
        mBookId = getIntent().getStringExtra("DOUBANBOOKID");
        mPresenter.getDetail(mBookId);

        mBookSummary.setMaxHeight(mBookSummary.getLineHeight() * maxDescripLine);
        //方法1
        mSummaryExpandableLayout.setOnClickListener(new View.OnClickListener() {
            boolean isExpand;//是否已展開的狀態(tài)

            @Override
            public void onClick(View v) {
                isExpand = !isExpand;
                mBookSummary.clearAnimation();//消除動畫效果
                final int deltaValue;//默認(rèn)高度灌旧,即前邊由maxLine確定的高度
                final int startValue = mBookSummary.getHeight();//起始高度
                int durationMillis = 200;//動畫持續(xù)時間
                if (isExpand) {
                    /**
                     * 折疊動畫
                     * 從實際高度縮回起始高度
                     */
                    deltaValue = mBookSummary.getLineHeight() * mBookSummary.getLineCount() - startValue;
                    RotateAnimation animation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
                    animation.setDuration(durationMillis);
                    animation.setFillAfter(true);
                    mSummaryExpandView.startAnimation(animation);
                } else {
                    /**
                     * 展開動畫
                     * 從起始高度增長至實際高度
                     */
                    deltaValue = mBookSummary.getLineHeight() * maxDescripLine - startValue;
                    RotateAnimation animation = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
                    animation.setDuration(durationMillis);
                    animation.setFillAfter(true);
                    mSummaryExpandView.startAnimation(animation);
                }
                Animation animation = new Animation() {
                    protected void applyTransformation(float interpolatedTime, Transformation t) { //根據(jù)ImageView旋轉(zhuǎn)動畫的百分比來顯示textview高度,達到動畫效果
                        mBookSummary.setHeight((int) (startValue + deltaValue * interpolatedTime));
                    }
                };
                animation.setDuration(durationMillis);
                mBookSummary.startAnimation(animation);
            }
        });

        mAuthorSummary = new MoreTextView(this, null);
    }

    private void initToolbar() {
        setSupportActionBar(mToolbar);
//        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//        getSupportActionBar().setHomeButtonEnabled(true);
    }

    @Override
    protected void onPostCreate(@Nullable Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        SwipeBackHelper.onPostCreate(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        SwipeBackHelper.onDestroy(this);
    }

    @Override
    public void onStartGetData() {
        mPrograss.setVisibility(View.VISIBLE);
    }

    @Override
    public void onGetSearchSuccess(DoubanBookDetail doubanBookDetail) {
        mPrograss.setVisibility(View.GONE);
        bindView(doubanBookDetail);
    }

    private void bindView(DoubanBookDetail doubanBookDetail) {
        mToolbarLayout.setTitle(String.format("售價:%s",doubanBookDetail.price));
        Glide.with(this).load(doubanBookDetail.images.large).into(mIvTitle);
        mBookName.setText(doubanBookDetail.title);
        mBookSubtitle.setText(doubanBookDetail.subtitle);
        mBookAuthor.setText(String.format("作者:%s", doubanBookDetail.author));
        mBookPublisher.setText(String.format("出版社:%s", doubanBookDetail.publisher));
        mBookPubdate.setText(String.format("出版時間:%s", doubanBookDetail.pubdate));
        mBookRating.setText(doubanBookDetail.rating.average);
        mBookNumRaters.setText(String.format("%s人", doubanBookDetail.rating.numRaters));
        /*
         *  在OnCreate方法中定義設(shè)置的textView不會馬上渲染并顯示
         *  所以textview的getLineCount()獲取到的值一般都為零
         *  因此使用post會在其繪制完成后來對ImageView進行顯示控制
         *  而此處是在返回數(shù)據(jù)后設(shè)置绰筛。
         */
        mBookSummary.setText(doubanBookDetail.summary);
        mSummaryHint.setText("圖書簡介");
        mSummaryExpandView.setVisibility(mBookSummary.getLineCount()
                > maxDescripLine ? View.VISIBLE : View.GONE);
        /*
         *方法2 通過自定義View組合封裝
         * 不使用xml來定義layout枢泰,直接定義一個繼承LinearLayout的MoreTextView類
         * 這個類里邊添加TextView和ImageView。
         */
        mSummaryHint1.setText("作者簡介");
        mMoreTextView.setText(doubanBookDetail.authorIntro);
        /*
         *方法3 通過自定義View組合封裝
         * 使用xml來定義layout
         */
        MyTextView myTextView = new MyTextView(DoubanBookDetailActivity.this);
        myTextView.setTextTags("標(biāo)簽", doubanBookDetail.tags);
        mContentLinear.addView(myTextView);

        mRatingbar.setRating(Float.parseFloat(doubanBookDetail.rating.average) / 2f);
    }

    @Override
    public void onGetDataFailed(String error) {
        mPrograss.setVisibility(View.GONE);
        toast(error);
    }

    @Override
    protected DoubanBookDetailPresenterImp1 createPresenter() {
        return new DoubanBookDetailPresenterImp1(this);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_detail, menu);
        return true;
    }

    @Override
    protected int getLayoutRes() {
        return R.layout.activity_douban_book_detail;
    }
}

圖書詳情頁除了CollapsingToolbarLayout這些在知乎詳情頁已有的控件外铝噩,還有個折疊的textView實現(xiàn)衡蚂,分別展示了圖書簡介,作者簡介骏庸,熱門標(biāo)簽毛甲。同時也是用了三種不同的實現(xiàn)方法:

  • ** 直接在xml里寫布局,然后在activity中處理具被。**

  • ** 通過自定義View組合封裝玻募,不使用xml來定義layout,直接定義一個繼承LinearLayout的MoreTextView類一姿,這個類里邊添加TextView和ImageView七咧。**

  • ** 通過自定義View組合封裝跃惫,使用xml來定義layout。**

其中我覺得最簡單也最好用的是第三種艾栋,當(dāng)自定義功能要求不是很高時爆存,只是為了封裝復(fù)用,第三種就完全夠用了,美滋滋蝗砾。
給出兩個自定義的View代碼
widget.MoreTextView

public class MoreTextView extends LinearLayout {
    protected TextView contentView; //文本正文
    protected ImageView expandView; //展開按鈕

    //對應(yīng)styleable中的屬性
    protected int textColor;
    protected float textSize;
    protected int maxLine;
    protected String text;
    protected float lineSpacingMultiplier;
    protected int lineSpacingExtra;
    //默認(rèn)屬性值
    public int defaultTextColor = Color.BLACK;
    public int defaultTextSize = 12;
    public int defaultLine = 3;



    public MoreTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initalize(); //初始化并添加View
        initWithAttrs(context, attrs);//取值并設(shè)置
        bindListener();//綁定點擊事件

    }

    private void initalize() {
        setOrientation(VERTICAL); //設(shè)置垂直布局
        setGravity(Gravity.RIGHT); //右對齊
        //初始化textView并添加
        contentView = new TextView(getContext());
        addView(contentView, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        //初始化ImageView并添加
        expandView = new ImageView(getContext());
        expandView.setImageResource(R.drawable.ic_down_arrow);
        LinearLayout.LayoutParams linearParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        addView(expandView, linearParams);
    }

    private void initWithAttrs(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.MoreTextStyle);
        textColor = a.getColor(R.styleable.MoreTextStyle_textColor,
                defaultTextColor); //取顏色值先较,默認(rèn)defaultTextColor
        textSize = a.getDimensionPixelSize(R.styleable.MoreTextStyle_textSize, defaultTextSize);//取顏字體大小,默認(rèn)defaultTextSize
        maxLine = a.getInt(R.styleable.MoreTextStyle_maxLine, defaultLine);//取顏顯示行數(shù)悼粮,默認(rèn)defaultLine
        text = a.getString(R.styleable.MoreTextStyle_text);//取文本內(nèi)容
        lineSpacingExtra = a.getDimensionPixelSize(R.styleable.MoreTextStyle_lineSpacingExtra, 1);
        lineSpacingMultiplier = a.getFloat(R.styleable.MoreTextStyle_lineSpacingMultiplier,1f);
        //綁定到textView
        bindTextView(textColor,textSize,maxLine,text,lineSpacingExtra,lineSpacingMultiplier);

        a.recycle();//回收釋放
    }
    //綁定到textView
    protected void bindTextView(int color,float size,final int line,String text,float lineSpacingExtra,float lineSpacingMultiplier){
        contentView.setTextColor(color);
        contentView.setTextSize(TypedValue.COMPLEX_UNIT_PX,size); //為sp
        contentView.setText(text);
        contentView.setHeight(contentView.getLineHeight() * line);
        contentView.setLineSpacing(lineSpacingExtra,lineSpacingMultiplier);

        post(new Runnable() {
            @Override
            public void run() {
                expandView.setVisibility(contentView.getLineCount() > line ? View.VISIBLE : View.GONE);
            }
        });
    }

    private void bindListener() {
        setOnClickListener(new View.OnClickListener() {
            boolean isExpand;
            @Override
            public void onClick(View v) {
                if (contentView.getLineCount() > maxLine) {
                    isExpand = !isExpand;
                    contentView.clearAnimation();
                    final int deltaValue;
                    final int startValue = contentView.getHeight();
                    int durationMillis = 350;
                    if (isExpand) {
                        deltaValue = contentView.getLineHeight() * contentView.getLineCount() - startValue;
                        RotateAnimation animation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
                        animation.setDuration(durationMillis);
                        animation.setFillAfter(true);
                        expandView.startAnimation(animation);
                    } else {
                        deltaValue = contentView.getLineHeight() * maxLine - startValue;
                        RotateAnimation animation = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
                        animation.setDuration(durationMillis);
                        animation.setFillAfter(true);
                        expandView.startAnimation(animation);
                    }
                    Animation animation = new Animation() {
                        protected void applyTransformation(float interpolatedTime, Transformation t) {
                            contentView.setHeight((int) (startValue + deltaValue * interpolatedTime));
                        }

                    };
                    animation.setDuration(durationMillis);
                    contentView.startAnimation(animation);
                }
            }
        });
    }

    public void setText(String string){

        bindTextView(textColor,textSize,maxLine,string,lineSpacingExtra,lineSpacingMultiplier);
    }
}

widget.MyTextView

public class MyTextView extends LinearLayout {
    @BindView(R.id.hint)
    TextView hint;
    @BindView(R.id.description_view)
    TextView contentView;
    @BindView(R.id.expand_view)
    ImageView expandView;
    @BindView(R.id.expandable_layout)
    LinearLayout expandableLayout;

    private int maxLine = 3;

    private Context mContext;

    public MyTextView(Context context) {
        this(context, null);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }

    private void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.textview_expandable, this);
        ButterKnife.bind(this, this);
        expandableLayout.setOnClickListener(new View.OnClickListener() {
            boolean isExpand;
            @Override
            public void onClick(View v) {
                isExpand = !isExpand;
                contentView.clearAnimation();
                final int deltaValue;
                final int startValue = contentView.getHeight();
                int durationMillis = 350;
                if (isExpand) {
                    deltaValue = contentView.getLineHeight() * contentView.getLineCount() - startValue;
                    RotateAnimation animation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
                    animation.setDuration(durationMillis);
                    animation.setFillAfter(true);
                    expandView.startAnimation(animation);
                } else {
                    deltaValue = contentView.getLineHeight() * maxLine - startValue;
                    RotateAnimation animation = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
                    animation.setDuration(durationMillis);
                    animation.setFillAfter(true);
                    expandView.startAnimation(animation);
                }
                Animation animation = new Animation() {
                    protected void applyTransformation(float interpolatedTime, Transformation t) {
                        contentView.setHeight((int) (startValue + deltaValue * interpolatedTime));
                    }

                };
                animation.setDuration(durationMillis);
                contentView.startAnimation(animation);
            }
        });
    }

    public void setTextTags(String title, List<DoubanBookDetail.TagsEntity> tagsEntityList) {
        hint.setText(title);
        for (int i = 0;i < tagsEntityList.size();i++) {
            contentView.setText(contentView.getText() + " " + String.format("%s(%s)",tagsEntityList.get(i).name,tagsEntityList.get(i).count));
        }

        expandView.post(new Runnable() {

            @Override
            public void run() {
                expandView.setVisibility(contentView.getLineCount() > maxLine ? View.VISIBLE : View.GONE);
            }
        });
        contentView.setHeight(contentView.getLineHeight() * maxLine);
    }

    public void setText(String title, String content) {
        hint.setText(title);
        contentView.setText(content);
        expandView.post(new Runnable() {
            @Override
            public void run() {
                expandView.setVisibility(contentView.getLineCount() > maxLine ? View.VISIBLE : View.GONE);
            }
        });
        contentView.setHeight(contentView.getLineHeight() * maxLine);
    }
}

完成圖書的功能后闲勺,我們就可以開始敲電影功能的代碼了。

因為m層矮锈,p層除了對應(yīng)的業(yè)務(wù)邏輯不同,大體實現(xiàn)都是一個套路(還可以對有著類似業(yè)務(wù)邏輯的mvp層再次封裝睁蕾?)苞笨,所以之后的內(nèi)容我只會給出相應(yīng)的方法實現(xiàn)并解釋,而不是無腦的把mvp三層的實現(xiàn)代碼copy下來(這樣貌似更麻煩了子眶,給自己挖了個坑瀑凝?)。之后的文章我也只會在第一次給出mvp三層實現(xiàn)代碼的樣例臭杰。

2. DoubanMovieFragment

豆瓣電影頁面的主要布局就是水平滾動的控件粤咪,來顯示不同要求下的電影列表
而DoubanMovieFragment中和上面的功能中多出的也是這一塊

先分析下怎么實現(xiàn)水平滾動的頁面,常見的有以下兩種(你是只知道這兩個吧- -,魂淡):

  • Recycleview
    直接設(shè)置Recycleview的布局管理器為LinearLayoutManager.HORIZONTAL渴杆,然后自己寫適配器寥枝。
  • HorizontalScrollView
    只能有一個子布局,添加為水平的LinearLayout磁奖,然后動態(tài)添加子布局囊拜。

這里我們選取的是第二種(希望大家能告訴我更多的實現(xiàn)方式,小白渴望更多姿勢)比搭。
首先我們在fragment布局文件寫個ProgressBar和RecyclerView冠跷,
在RecyclerView的adapter中寫InTheatersViewHolder和Top250ViewHolder,
然后綁定他們的視圖身诺,他們的視圖就是由RelativeLayout(提示框)和HorizontalScrollView構(gòu)成,adapter中有setData的方法蜜托,當(dāng)fragment中調(diào)用adapter的setData方法,就更新數(shù)據(jù)并notifyItemChanged(相應(yīng)的item)霉赡。

下面給出適配器的代碼:
adapter.DoubanMovieAdapter

public class DoubanMovieAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private Context mContext;
    private static final int TYPE_InTheaters = 0;
    private static final int TYPE_Top250 = 1;
    private DoubanMovieDetail mDoubanMovieDetail;
    private DoubanMovieDetail mDoubanMovieTopDetail;

    private OnItemClickListener mItemClickListener;

    public DoubanMovieAdapter(Context context) {
        mContext = context;
    }

    public void setInTheatersData(DoubanMovieDetail data) {
        mDoubanMovieDetail = data;
        notifyItemChanged(TYPE_InTheaters);
    }

    public void setTopData(DoubanMovieDetail data) {
        mDoubanMovieTopDetail = data;
        notifyItemChanged(TYPE_Top250);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch (viewType) {
            case TYPE_InTheaters:
                return new InTheatersViewHolder(
                        LayoutInflater.from(mContext).inflate(
                                R.layout.douban_movie_intheaters,parent,false)
                );
            case TYPE_Top250:
                return new Top250ViewHolder(
                        LayoutInflater.from(mContext).inflate(
                            R.layout.douban_movie_intheaters,parent,false)
                );
        }
        return null;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == DoubanMovieAdapter.TYPE_InTheaters) {
            return DoubanMovieAdapter.TYPE_InTheaters;
        }else if (position == DoubanMovieAdapter.TYPE_Top250) {
            return DoubanMovieAdapter.TYPE_Top250;
        }
        return super.getItemViewType(position);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
        int itemType = getItemViewType(position);
        switch (itemType) {
            case TYPE_InTheaters:
                ((InTheatersViewHolder) holder).bind(mDoubanMovieDetail);
                break;
            case TYPE_Top250:
                ((Top250ViewHolder) holder).bind(mDoubanMovieTopDetail);
                break;
            default:
                break;
        }
    }

    @Override
    public int getItemCount() {
        return 2;
    }

    class InTheatersViewHolder extends RecyclerView.ViewHolder {
        public LinearLayout movieScrollView;
        public TextView hint;
        public TextView more;
        @BindView(R.id.iv_movie)
        ImageView ivMovie;
        @BindView(R.id.tv_movie_name)
        TextView tvMovieName;
        @BindView(R.id.tv_directors)
        TextView tvDirectors;
        @BindView(R.id.tv_casts)
        TextView tvCasts;
        @BindView(R.id.tv_Rating)
        TextView tvRating;

        public InTheatersViewHolder(View itemView) {
            super(itemView);
            hint = (TextView) itemView.findViewById(R.id.hint);
            more = (TextView) itemView.findViewById(R.id.tv_more_intheaters);
            hint.setText("正在熱映");
            movieScrollView = (LinearLayout) itemView.findViewById(R.id.sv_add);
        }

        protected void bind(DoubanMovieDetail doubanMovieDetail) {
            int size = mDoubanMovieDetail == null ? 0 : mDoubanMovieDetail.subjects.size();
            for (int i = 0; i < size; i++) {
                View view = View.inflate(mContext, R.layout.item_douban_movie_intheater, null);
                ButterKnife.bind(this, view);
                try {
                    Glide.with(mContext)
                            .load(doubanMovieDetail.subjects.get(i).images.large)
                            .into(ivMovie);
                    tvMovieName.setText(doubanMovieDetail.subjects.get(i).title);
                    tvDirectors.setText(String.format("導(dǎo)演:%s", doubanMovieDetail.subjects.get(i).directors.get(0).name));
                    tvCasts.setText(String.format("主演:%s", doubanMovieDetail.subjects.get(i).casts.get(0).name));
                    tvRating.setText(String.format("評分:%s", doubanMovieDetail.subjects.get(i).rating.average));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                final int finalI = i;
                view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mItemClickListener != null) {
                            mItemClickListener.onItemClick(mDoubanMovieDetail.subjects.get(finalI).id, TYPE_InTheaters);
                        }
                    }
                });
                movieScrollView.addView(view);
            }
        }
    }

    class Top250ViewHolder extends RecyclerView.ViewHolder{
        public LinearLayout mAddView;
        public TextView hint;
        public TextView more;

        @BindView(R.id.iv_movie)
        ImageView ivMovie;
        @BindView(R.id.tv_movie_name)
        TextView tvMovieName;
        @BindView(R.id.tv_directors)
        TextView tvDirectors;
        @BindView(R.id.tv_casts)
        TextView tvCasts;
        @BindView(R.id.tv_Rating)
        TextView tvRating;
        public Top250ViewHolder(View itemView) {
            super(itemView);
            hint = (TextView) itemView.findViewById(R.id.hint);
            more = (TextView) itemView.findViewById(R.id.tv_more_intheaters);
            hint.setText("豆瓣Top250");
            mAddView = (LinearLayout) itemView.findViewById(R.id.sv_add);
        }

        protected void bind(DoubanMovieDetail doubanMovieDetail) {
            int size = mDoubanMovieTopDetail == null ? 0 : mDoubanMovieTopDetail.subjects.size();
           for (int i = 0; i < size; i++) {
                View view = View.inflate(mContext, R.layout.item_douban_movie_intheater, null);
                ButterKnife.bind(this, view);
                try {
                    Glide.with(mContext)
                            .load(doubanMovieDetail.subjects.get(i).images.large)
                            .into(ivMovie);
                    tvMovieName.setText(doubanMovieDetail.subjects.get(i).title);
                    tvDirectors.setText(String.format("導(dǎo)演:%s", doubanMovieDetail.subjects.get(i).directors.get(0).name));
                    tvCasts.setText(String.format("主演:%s", doubanMovieDetail.subjects.get(i).casts.get(0).name));
                    tvRating.setText(String.format("評分:%s", doubanMovieDetail.subjects.get(i).rating.average));

                } catch (Exception e) {
                    e.printStackTrace();
                }
                final int finalI = i;
                view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mItemClickListener != null) {
                            mItemClickListener.onItemClick(mDoubanMovieTopDetail.subjects.get(finalI).id, TYPE_Top250);
                        }
                    }
                });
               mAddView.addView(view);
            }
        }
    }

    public interface OnItemClickListener {
        void onItemClick(String id, int Type);
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        mItemClickListener = listener;
    }
}

xml等細(xì)節(jié)詳見github上的代碼橄务。

3. DoubanMusicFragment

其實DoubanMusicFragment里面沒什么新的東西,只是使用了GridLayout布局穴亏,和弄了個標(biāo)簽?zāi)拥腂utton,然后在adapter里添加相應(yīng)的button布局仪糖,標(biāo)簽內(nèi)容直接在fragment中寫了個數(shù)組傳給adapter柑司,adapter中寫了個接口回調(diào)點擊事件,來加載相應(yīng)音樂類型的結(jié)果锅劝。然后結(jié)果內(nèi)容的呈現(xiàn)我是直接抄我上面bookFragment中搜索結(jié)果的代碼來完成的攒驰。所以,DoubanMusicFragment的介紹完成故爵。此致玻粪,敬禮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末诬垂,一起剝皮案震驚了整個濱河市劲室,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌结窘,老刑警劉巖很洋,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異隧枫,居然都是意外死亡喉磁,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門官脓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來协怒,“玉大人,你說我怎么就攤上這事卑笨≡邢荆” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵赤兴,是天一觀的道長妖滔。 經(jīng)常有香客問我,道長桶良,這世上最難降的妖魔是什么铛楣? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮艺普,結(jié)果婚禮上簸州,老公的妹妹穿的比我還像新娘。我一直安慰自己歧譬,他們只是感情好岸浑,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瑰步,像睡著了一般矢洲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缩焦,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天读虏,我揣著相機與錄音责静,去河邊找鬼。 笑死盖桥,一個胖子當(dāng)著我的面吹牛灾螃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播揩徊,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼腰鬼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了塑荒?” 一聲冷哼從身側(cè)響起熄赡,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎齿税,沒想到半個月后彼硫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡凌箕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年拧篮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陌知。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡他托,死狀恐怖掖肋,靈堂內(nèi)的尸體忽然破棺而出仆葡,到底是詐尸還是另有隱情,我是刑警寧澤志笼,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布沿盅,位于F島的核電站,受9級特大地震影響纫溃,放射性物質(zhì)發(fā)生泄漏腰涧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一紊浩、第九天 我趴在偏房一處隱蔽的房頂上張望窖铡。 院中可真熱鬧,春花似錦坊谁、人聲如沸费彼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箍铲。三九已至,卻和暖如春鬓椭,著一層夾襖步出監(jiān)牢的瞬間颠猴,已是汗流浹背关划。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留翘瓮,地道東北人贮折。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像春畔,于是被迫代替她去往敵國和親脱货。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

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