上路傳送眼:
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的介紹完成故爵。此致玻粪,敬禮。