上路传送眼:
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的介绍完成。此致,敬礼。