快速完成一個新聞APP
本Demo主要使用的技術(shù):
- 看標題就知道了
- Material Design
- 聚合數(shù)據(jù)
效果
直接點吧,先看下效果
Demo架構(gòu)
老司機們一看就知道界面是由ViewPager+Fragment組成,還是比較簡單的嘶伟。新聞詳情頁面主要是采用了design包下的CoordinatorLayout作為父布局赊豌,因為要做出那個下拉折疊效果嘛壤蚜。然后點擊新聞列表時會有一個轉(zhuǎn)場動畫邻悬,不知道細心的朋友們有木有看出來撞蚕,上拉刷新是采用的官方的SwipeRefreshLayout
一切都追求原滋原味验庙。
整個Demo的網(wǎng)絡(luò)請求是通過RxJava+Retrofit來實現(xiàn)的顶吮,為什么用這對基友組合呢?
三個字 “太爽了”
OK粪薛,后面會有相關(guān)的介紹悴了。
聚合數(shù)據(jù)的key請自己申請,我的已經(jīng)過期了违寿,在Config里面配置下就好了湃交。
代碼分析
封裝BaseActivity
雖然整個Demo就兩個Activity,那我們還是封裝一下藤巢,因為我喜歡追求代碼簡潔(與后面可能會有些出入)額額搞莺,,
先上波代碼吧
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initContentView(savedInstanceState);
initStatusBar();
}
protected abstract void initContentView(Bundle savedInstanceState);
/**
* 初始化沉浸式狀態(tài)欄
*/
private void initStatusBar(){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){//4.4 全透明狀態(tài)欄
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0 全透明實現(xiàn)
Window window = getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);//calculateStatusColor(Color.WHITE, (int) alphaValue)
}
}
@SuppressWarnings("unchecked")
public final <E extends View> E findView(int id){
try {
return (E) findViewById(id);
}catch (ClassCastException e){
throw e;
}
}
}
其實也沒什么亮點掂咒,就是封裝下共同的方法才沧,一個是沉浸式狀態(tài)欄,一個是我為了偷懶绍刮,不想在findviewById的時候加個強制類型轉(zhuǎn)換温圆。當然也可以通過框架注入,和databinding來解決這個孩革,但這次的重點不是他們岁歉。
創(chuàng)建實體類
對了,這些數(shù)據(jù)都是從聚合數(shù)據(jù)獲取下來的膝蜈,具體細節(jié)就不多說了锅移,就是申請個key熔掺,填寫一些參數(shù),然后它會返回一串json數(shù)據(jù)非剃。我們就通過這些json數(shù)據(jù)去生成對應(yīng)的實體類瞬女,用一個良心之作的工具 GsonFormat,具體操作可以自行Google或者百度努潘。
使用這個工具一是為了偷懶诽偷,二是為了配合gson來對json解析。
創(chuàng)建配置文件
項目中可能會遇到一些很多地方都會用到的常量疯坤,比如說聚合數(shù)據(jù)的key等等报慕,我們可以創(chuàng)建一個接口
將這些數(shù)據(jù)寫到這個接口里面,這樣的話压怠,哪里要用就直接繼承這個接口就OK了眠冈。
public interface Config {
String[] ARRYTITLES ={"頭條","社會","科技","國內(nèi)","國際","娛樂","時尚","軍事","體育","財經(jīng)"};
String KEY_POSTION="key_postion";
String[] ARRYTYPE={"top","shehui","keji","guonei","guoji","yule","shishang","junshi","tiyu","caijing"};
String KEY_IMG_URL="imgurl";
String KEY_CONTENT_URL="contenturl";
String KEY_TYPE="type";
String KEY_JUHE="0489bcea378ce792facda791d0f1e188";
}
因為請求不同的類型的新聞,參數(shù)不一樣菌瘫,所以弄個數(shù)組蜗顽,把參數(shù)存入進去,記得與類型對應(yīng)雨让。
主Activity編寫
這里因為創(chuàng)建項目的時候手賤了下雇盖,點了那個有側(cè)滑的activity,所以一些生成了很多沒什么卵用的代碼(至少這個項目里面沒什么用)
public class MainActivity extends BaseActivity
implements NavigationView.OnNavigationItemSelectedListener,Config {
private TabLayout mTabLayout;
private ViewPager mViewPager;
private ViewPagerAdapter mAdapter;
private List<String> mTitles=new ArrayList<>();
@Override
protected void initContentView(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
Toolbar toolbar = findView(R.id.toolbar);
setSupportActionBar(toolbar);
mTabLayout=findView(R.id.tab_layout);
mViewPager=findView(R.id.viewpager);
DrawerLayout drawer = findView(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawer.setDrawerListener(toggle);
toggle.syncState();
NavigationView navigationView = findView(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
initTitle();
mAdapter=new ViewPagerAdapter(getSupportFragmentManager(),mTitles);
mViewPager.setAdapter(mAdapter);
mTabLayout.setupWithViewPager(mViewPager);
mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
}
private void initTitle(){
for (int i=0;i<ARRYTITLES.length;i++){
mTitles.add(ARRYTITLES[i]);
}
}
}
這里就是進行初始化一些view栖忠,將fragment添加進去崔挖,viewpager+tablayout基友組合。
創(chuàng)建Fragment的適配器
先說說適配器吧庵寞,因為這里要用的fragment比較多狸相,所以繼承FragmentStatePagerAdapter,Why捐川?
內(nèi)存優(yōu)化
因為一個Fragment占的內(nèi)存還是比較大脓鹃,一旦fragment數(shù)量比較多了,后果你懂的古沥。當頁面不可見時瘸右, 對應(yīng)的Fragment實例可能會被銷毀,但是Fragment的狀態(tài)會被保存渐白,所以一些提高了我們的app性能尊浓。
public class ViewPagerAdapter extends FragmentStatePagerAdapter {
private List<String> mTitles;
public ViewPagerAdapter(FragmentManager fm, List<String> mTitles) {
super(fm);
this.mTitles = mTitles;
}
@Override
public Fragment getItem(int position) {
return ContentFragment.instance(position);
}
@Override
public int getCount(){
return mTitles.size();
}
@Override
public CharSequence getPageTitle(int position) {
return mTitles.get(position);
}
}
還是比較簡單的,可能最后一個方法或許有些陌生纯衍,用過TabLayout的朋友應(yīng)該懂,就是設(shè)置Tab的標題苗胀,因為我們的ViewPager是要與TabLayout進行關(guān)聯(lián)的襟诸。
編寫網(wǎng)絡(luò)工具類
好了重頭戲來了瓦堵,也是本項目唯一的特色,RxJava+Retrofit歌亲。
記得別忘了引入這些框架
compile 'io.reactivex:rxjava:1.0.14'
compile 'io.reactivex:rxandroid:1.0.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.google.code.gson:gson:2.6.2'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
這里我不做過多關(guān)于RxJava和Retrofit的描述菇用,因為相關(guān)資料網(wǎng)上一堆堆。RxJava說到底就是異步陷揪,這是它整個流程非常簡潔明了惋鸥,而且方便線程切換。Retrofit是講OKHttp更好的封裝下悍缠,簡化我們的網(wǎng)絡(luò)請求卦绣。
首先編寫Retrofit接口
public interface NewService {
String BASE_URL="http://v.juhe.cn/";
@GET("toutiao/index?")
Observable<News> getNews(@QueryMap Map<String,String> map);
}
好像News 少了個s
接下來編寫我們的請求工具類
private static final int DEFAULT_TIMEOUT = 5;
private Retrofit retrofit;
private NewService newService;
private RetrofitUtil(){
//手動創(chuàng)建一個OkHttpClient并設(shè)置超時時間
OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
httpClientBuilder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
Gson gson = new GsonBuilder()
//配置Gson
.setDateFormat("yyyy-MM-dd hh:mm:ss")
.create();
retrofit=new Retrofit.Builder()
.baseUrl(NewService.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
newService=retrofit.create(NewService.class);
}
//在訪問HttpMethods時創(chuàng)建單例
private static class SingletonHolder{
private static final RetrofitUtil INSTANCE = new RetrofitUtil();
}
public static RetrofitUtil getInstance(){
return SingletonHolder.INSTANCE;
}
public void getNews(Subscriber<News> newsSubscriber,int type){
newService.getNews(getParams(type))
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(newsSubscriber);
}
private Map<String,String> getParams(int type){
Map<String,String> map=new HashMap<>();
map.put("type",ARRYTYPE[type]);
map.put("key",KEY_JUHE);
return map;
}
}
我們在外面只用調(diào)用getNews就行了,傳一個Subscriber和類型就行了飞蚓。邏輯都不是很復(fù)雜吧滤港。里面的精髓就是線程切換 .subscribeOn(Schedulers.io()) ,RxJava給我嗎提供了五個選擇趴拧,這里因為我們是請求網(wǎng)絡(luò)溅漾,所以就用io的,最后切換到主線程 .observeOn(AndroidSchedulers.mainThread())
編寫Fragment
既然網(wǎng)絡(luò)工具類寫好了著榴,那么就寫個fragment來把這些數(shù)據(jù)展示出來吧添履!
public class ContentFragment extends Fragment implements Config {
private int mType;
private List<News.ResultBean.DataBean> mData;
private SwipeRefreshLayout mRefreshLayout;
private RecyclerView mShowNews;
private NewsAdapter mAdapter;
private int mSpacingInPixels;//
private int mCount=0;
public static Fragment instance(int postion){
ContentFragment fragment=new ContentFragment();
Bundle bundle = new Bundle() ;
bundle.putInt(KEY_POSTION,postion);
fragment.setArguments(bundle);
return fragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.fragment_content,container,false);
return view;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Bundle bundle = getArguments() ;
Log.e("dandy","pos "+bundle.getInt(KEY_POSTION));
initViews(view);
mType=bundle.getInt(KEY_POSTION);
getData();
}
private void initViews(View view) {
mRefreshLayout= (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
mShowNews= (RecyclerView) view.findViewById(R.id.news_recyclerview);
mRefreshLayout.setColorSchemeResources(R.color.colorPrimary,R.color.tab_select_text_color,R.color.refresh_color,R.color.colorAccent);
mRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
getData();
}
});
mSpacingInPixels= getResources().getDimensionPixelSize(R.dimen.item_space);
mShowNews.setHasFixedSize(true);
}
private void getData(){
mRefreshLayout.setRefreshing(true);
Subscriber<News> subscriber=new Subscriber<News>() {
@Override
public void onCompleted() {
}
//出現(xiàn)異常回調(diào)
@Override
public void onError(Throwable e) {
Log.e("smile","獲取失敗");
}
//獲取數(shù)據(jù)成功后回調(diào)
@Override
public void onNext(News news) {
Log.e("smile","獲取出來的"+news.getResult().getData().size());
//mData=news.getResult().getData();
setData(news);
}
};
RetrofitUtil.getInstance().getNews(subscriber,mType);
}
private void setData(News data){
mData=data.getResult().getData();
mRefreshLayout.setRefreshing(false);
mAdapter=new NewsAdapter(getContext(),data);
mShowNews.setLayoutManager(new LinearLayoutManager(getContext()));
//避免重復(fù)添加間距
if (mCount==0){
mShowNews.addItemDecoration(new SpacesItemDecoration(mSpacingInPixels));
}
mShowNews.setAdapter(mAdapter);
// mAdapter.setOnScrollListener(mShowNews);
mAdapter.setOnItemClickListener(new NewsAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
startDetailActivity(view,mData.get(position));
}
@Override
public void onItemLongClick(View view, int position) {
}
}) ;
mCount++;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void startDetailActivity(View view, News.ResultBean.DataBean bean){
Intent intent=new Intent(getActivity(), NewsDetailActivity.class);
Bundle bundle=new Bundle();
bundle.putString(KEY_IMG_URL,bean.getThumbnail_pic_s());
bundle.putString(KEY_CONTENT_URL,bean.getUrl());
bundle.putString(KEY_TYPE,ARRYTITLES[mType]);
intent.putExtras(bundle);
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(),view.findViewById(R.id.item_news_img),"photos");
getContext().startActivity( intent, options.toBundle());
}
}
其實仔細一看也不是很復(fù)雜脑又,就是調(diào)用我們開始寫的getNews而已缝龄,請求成功后會回調(diào)onNext方法。對了這里有個轉(zhuǎn)場動畫挂谍,就是最后一個方法叔壤,這里的動畫效果是共享元素,所以指定你要共享的元素就行口叙,然后設(shè)置下它的 android:transitionName="photos"迎变。
編寫RecyclerView適配器
再見ListView蕉世,你好RecyclerView
RecyclerView的優(yōu)點就不多說了,就是自由,任性
適配器堕义,我就不貼代碼了,累羊娃,而且沒什么特色翼馆。
編寫詳情頁面
這里主要都是用了Design里面的一些控件
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="250dp">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="?attr/actionBarSize"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:statusBarScrim="?attr/colorAccent"
app:collapsedTitleGravity="left"
app:expandedTitleGravity="center_horizontal|bottom"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="@+id/news_img"
android:src="@mipmap/ic_launcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:transitionName="photos"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
app:layout_collapseMode="pin"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="?attr/homeAsUpIndicator"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
style="@style/ToolbarTheme"
android:titleTextColor="@color/white"
/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<com.dandy.smilenews.ui.MyNestedScrollView
android:id="@+id/news_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<WebView
android:id="@+id/news_web"
android:layout_width="match_parent"
android:layout_height="match_parent"></WebView>
</com.dandy.smilenews.ui.MyNestedScrollView>
</android.support.design.widget.CoordinatorLayout>
折疊式效果就是通過編寫xml就能實現(xiàn)了,還是簡單的描述下那些屬性的含義
CollapsingToolbarLayout
//折疊后的背景色 -> setContentScrim(Drawable)
app:contentScrim="?attr/colorPrimary"
// 必須設(shè)置透明狀態(tài)欄才有效 -> setStatusBarScrim(Drawable)
app:statusBarScrim="?attr/colorAccent"
// 標題
app:title="title"
// 折疊后的標題位置
app:collapsedTitleGravity="right"
// 打開時的標題位置
app:expandedTitleGravity="center_horizontal|bottom"
折疊效果
app:layout_collapseMode
有兩個可選:
parallax —— 視差模式,就是上面的圖片的變化效果
pin —— 固定模式启具,在折疊的時候最后固定在頂端
// 視差效果
app:layout_collapseParallaxMultiplier
范圍[0.0,1.0]本讥,值越大視差越大
下面的MyNestedScrollView是我自定義的一個view,繼承的NestedScrollView,主要是為了解決事件沖突導(dǎo)致滑動卡頓拷沸,就是不讓攔截子view的觸摸事件色查。
class MyNestedScrollView extends NestedScrollView {
private GestureDetector mGestureDetector;
View.OnTouchListener mGestureListener;
public MyNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyNestedScrollView(Context context) {
super(context);
}
public MyNestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
mGestureDetector = new GestureDetector(context, new YScrollDetector());
setFadingEdgeLength(0);
}
class YScrollDetector extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceY) > Math.abs(distanceX)) {
return true;
}
return false;
}
}
}
到這里差不多也要完工了,就可以看到開頭的效果了撞芍,貌似第一次寫這么長的博客秧了,,序无,
最后附上源碼 傳送門
喜歡就給個星星吧验毡!