Android练手小项目(KTReader)基于mvp架构(四)

上路传送眼:

Android练手小项目(KTReader)基于mvp架构(三)

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

上篇文章中我们完成了知乎日报详情页。
而这次我们要做的的就是完成整个豆瓣模块(这篇文章有点长,请先马后看)。

效果图献上:


效果图

从效果图中可以看到,这次我们加入的豆瓣模块采用了常见的TabLayout+ViewPager实现界面的切换(此处有个坑,后文会提到),子fragment实现的功能如下:

  • 图书:

图书页我们放置了一个SearchView,让用户输入图书名后获得返回结果,点击进入详情页。
用到的新东西:

SearchView
折叠textview+标题布局(三种方法)

  • 电影:

电影页我们放置了两个HorizontalScrollView实现水平滚动分别显示正在热映的电影和豆瓣电影的top250(充满恶意的想把top去掉)。点击进入相应的电影详情页。
用到的新东西:

HorizontalScrollView
RecyclerView.Adapter下多个ViewHolder

  • 音乐:

音乐页我们放置了一个标签列表,点击相应标签,呈现相应音乐类型的结果,并没有详情页(没错,就是懒,之后应该会补上,我会说原本还有个同城页直接被我删了fragment么2333)。
用到的新东西:

RecyclerView的GridLayout布局
标签的最简单实现(一个button硬是说成新东西,也是没谁)

OK,前面废话了那么多,下面给出具体实现方法

首先,我们要先写好抽屉菜单的布局,这里就不给布局代码了,简单提两句,就是两个group,第一个中有四个模块(知乎,豆瓣,奇闻,壁纸),第二个为个性化(足迹,收藏,设置,关于)。之后在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;
    }
}

豆瓣模块的网络请求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关键字
    @GET("book/search")
    Observable<String> getSearchBookByTag(@Query("tag") String text); //搜索图书by类型

    /**
     * 电影Api
     */
    @GET("movie/search")
    Observable<String> getSearchMovie(@Path("q") String text);//获取电影条目搜索结果数据
    @GET("movie/in_theaters")
    Observable<DoubanMovieDetail> getInTheaters();//获取热映电影数据
    @GET("movie/coming_soon")
    Observable<String> getComingSonn();//获取即将即将上映电影数据
    @GET("movie/top250")
    Observable<DoubanMovieDetail> getTop250(@Query("start") String start);//获取Top250数据
    @GET("movie/weekly")
    Observable<String> getWeekly();//获取口碑榜数据
    @GET("movie/new_movies")
    Observable<String> getNewMovies();//获取新片榜数据
    @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关键字
    @GET("music/{id}")
    Observable<DoubanMusic> getSearchMusicById(@Path("text")  String id); //搜索音乐
}

RetrofitManager中需要添加新的获取服务方法:
utils.RetrofitManager:

public DoubanApi getDoubanService(String url) {
        if (doubanApi == null) {
            doubanApi = new Retrofit.Builder()
                    .baseUrl(url) //必须以‘/’结尾
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//使用RxJava2作为CallAdapter
                    .client(client)//如果没有添加,那么retrofit2会自动给我们添加了一个。
                    .addConverterFactory(GsonConverterFactory.create())//Retrofit2可以帮我们自动解析返回数据,
                    .build().create(DoubanApi.class);
        }
        return doubanApi;
    }

准备工作做完后,
下面开始进行DoubanFragment的实现
这里先讲讲所遇见的坑:

fragment+tablayout+viewpager+多个fragment内容不显示

具体表现就是点击豆瓣精选,后点击知乎专栏,再点击回豆瓣精选,发现豆瓣页的内容神奇的被整没了。
究其原因是由于嵌套fragment产生了这个bug,当寄生fragment销毁视图时,其内部的子fragment并没有销毁,然后再次生成这个fragment时,其子fragment的视图不会重建。
查了好久资料,都与自己的情况不符(用的support v4包),最后换个方式直接寻找销毁子fragment的方法,最终解决了表面的问题,解决方案在DoubanFragment的removeChildFragment方法里。

ps:崩溃的是解决了这个,然后详情页点击返回按钮时应用又崩了,
机智的我就把返回按钮给变没了,反正可以右滑返回不是么(滑稽脸)。
不过偷懒不是好孩子,以后还要回来根除这个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内容不显示
     * 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实现了滑动更改页面的功能
下面给出适配器:
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写好之后就要写各个页面的具体实现了:

1.DoubanBookFragment

Model层

  • 模型实体类DoubanBook惯例直接GsonFormat工具生成
    (model.entity.DoubanBook)

  • 豆瓣图书Model接口
    model.ZhihuDetailModel:

public interface DoubanBookModel {

    void loadSearch(String id, OnDoubanBookListener listener);//图书搜索
}
  • 豆瓣图书Model具体实现类
    model.imp1.DoubanBookModelImp1
public class DoubanBookModelImp1 implements DoubanBookModel {

    private DoubanApi mDoubanApiService; //请求服务

    public DoubanBookModelImp1() {
        mDoubanApiService = RetrofitManager
                .getInstence()
                .getDoubanService(Constant.DOUBAN_BASE_URL); //创建请求服务
    }
    @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层

  • 回调接口
    view.DoubanBookView
public interface DoubanBookView {
    void onStartGetData();

    void onGetSearchSuccess(DoubanBook doubanBook);

    void onGetDataFailed(String error);
}
  • 创建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个相关结果",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中写了个个回调接口,当图书某项被点击时,调用回调实现activity的跳转,跳转到图书详情页。

  • Presenter层

  • 回调接口
    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的实现
    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实现基本一致):
activity.DoubanBookDetailActivity

public class DoubanBookDetailActivity extends MVPBaseActivity<DoubanBookDetailView, DoubanBookDetailPresenterImp1> implements DoubanBookDetailView {
    //大大的PS:BindView代码我删去了 太长
    MoreTextView mAuthorSummary;
    private int maxDescripLine = 3; //TextView默认最大展示行数
    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;//是否已展开的状态

            @Override
            public void onClick(View v) {
                isExpand = !isExpand;
                mBookSummary.clearAnimation();//消除动画效果
                final int deltaValue;//默认高度,即前边由maxLine确定的高度
                final int startValue = mBookSummary.getHeight();//起始高度
                int durationMillis = 200;//动画持续时间
                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) { //根据ImageView旋转动画的百分比来显示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方法中定义设置的textView不会马上渲染并显示
         *  所以textview的getLineCount()获取到的值一般都为零
         *  因此使用post会在其绘制完成后来对ImageView进行显示控制
         *  而此处是在返回数据后设置。
         */
        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("标签", 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实现,分别展示了图书简介,作者简介,热门标签。同时也是用了三种不同的实现方法:

  • ** 直接在xml里写布局,然后在activity中处理。**

  • ** 通过自定义View组合封装,不使用xml来定义layout,直接定义一个继承LinearLayout的MoreTextView类,这个类里边添加TextView和ImageView。**

  • ** 通过自定义View组合封装,使用xml来定义layout。**

其中我觉得最简单也最好用的是第三种,当自定义功能要求不是很高时,只是为了封装复用,第三种就完全够用了,美滋滋。
给出两个自定义的View代码
widget.MoreTextView

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

    //对应styleable中的属性
    protected int textColor;
    protected float textSize;
    protected int maxLine;
    protected String text;
    protected float lineSpacingMultiplier;
    protected int lineSpacingExtra;
    //默认属性值
    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);//取值并设置
        bindListener();//绑定点击事件

    }

    private void initalize() {
        setOrientation(VERTICAL); //设置垂直布局
        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); //取颜色值,默认defaultTextColor
        textSize = a.getDimensionPixelSize(R.styleable.MoreTextStyle_textSize, defaultTextSize);//取颜字体大小,默认defaultTextSize
        maxLine = a.getInt(R.styleable.MoreTextStyle_maxLine, defaultLine);//取颜显示行数,默认defaultLine
        text = a.getString(R.styleable.MoreTextStyle_text);//取文本内容
        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层除了对应的业务逻辑不同,大体实现都是一个套路(还可以对有着类似业务逻辑的mvp层再次封装?),所以之后的内容我只会给出相应的方法实现并解释,而不是无脑的把mvp三层的实现代码copy下来(这样貌似更麻烦了,给自己挖了个坑?)。之后的文章我也只会在第一次给出mvp三层实现代码的样例。

2. DoubanMovieFragment

豆瓣电影页面的主要布局就是水平滚动的控件,来显示不同要求下的电影列表
而DoubanMovieFragment中和上面的功能中多出的也是这一块

先分析下怎么实现水平滚动的页面,常见的有以下两种(你是只知道这两个吧- -,魂淡):

  • Recycleview
    直接设置Recycleview的布局管理器为LinearLayoutManager.HORIZONTAL,然后自己写适配器。
  • HorizontalScrollView
    只能有一个子布局,添加为水平的LinearLayout,然后动态添加子布局。

这里我们选取的是第二种(希望大家能告诉我更多的实现方式,小白渴望更多姿势)。
首先我们在fragment布局文件写个ProgressBar和RecyclerView,
在RecyclerView的adapter中写InTheatersViewHolder和Top250ViewHolder,
然后绑定他们的视图,他们的视图就是由RelativeLayout(提示框)和HorizontalScrollView构成,adapter中有setData的方法,当fragment中调用adapter的setData方法,就更新数据并notifyItemChanged(相应的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("导演:%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("导演:%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等细节详见github上的代码。

3. DoubanMusicFragment

其实DoubanMusicFragment里面没什么新的东西,只是使用了GridLayout布局,和弄了个标签模样的Button,然后在adapter里添加相应的button布局,标签内容直接在fragment中写了个数组传给adapter,adapter中写了个接口回调点击事件,来加载相应音乐类型的结果。然后结果内容的呈现我是直接抄我上面bookFragment中搜索结果的代码来完成的。所以,DoubanMusicFragment的介绍完成。此致,敬礼。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容