Skip to content

Smile52/SmileNews

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SmileNews

快速完成一个新闻APP

本Demo主要使用的技术:

  • 看标题就知道了
  • Material Design
  • 聚合数据

效果

直接点吧,先看下效果 这里写图片描述

Demo架构

老司机们一看就知道界面是由ViewPager+Fragment组成,还是比较简单的。新闻详情页面主要是采用了design包下的CoordinatorLayout作为父布局,因为要做出那个下拉折叠效果嘛。然后点击新闻列表时会有一个转场动画,不知道细心的朋友们有木有看出来,上拉刷新是采用的官方的SwipeRefreshLayout 一切都追求原滋原味。 整个Demo的网络请求是通过RxJava+Retrofit来实现的,为什么用这对基友组合呢? 三个字 “太爽了” OK,后面会有相关的介绍。

##代码分析

封装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);
    /**
     * 初始化沉浸式状态栏
     */
    private void initStatusBar(){
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){//4.4 全透明状态栏
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0 全透明实现
            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;
        }
    }
}

其实也没什么亮点,就是封装下共同的方法,一个是沉浸式状态栏,一个是我为了偷懒,不想在findviewById的时候加个强制类型转换。当然也可以通过框架注入,和databinding来解决这个,但这次的重点不是他们。

创建实体类 对了,这些数据都是从聚合数据获取下来的,具体细节就不多说了,就是申请个key,填写一些参数,然后它会返回一串json数据。我们就通过这些json数据去生成对应的实体类,用一个良心之作的工具 GsonFormat,具体操作可以自行Google或者百度。  使用这个工具一是为了偷懒,二是为了配合gson来对json解析。

创建配置文件 项目中可能会遇到一些很多地方都会用到的常量,比如说聚合数据的key等等,我们可以创建一个接口 将这些数据写到这个接口里面,这样的话,哪里要用就直接继承这个接口就OK了。

public interface Config {
     String[] ARRYTITLES ={"头条","社会","科技","国内","国际","娱乐","时尚","军事","体育","财经"};
     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";

}

因为请求不同的类型的新闻,参数不一样,所以弄个数组,把参数存入进去,记得与类型对应。

主Activity编写 这里因为创建项目的时候手贱了下,点了那个有侧滑的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基友组合。

创建Fragment的适配器 先说说适配器吧,因为这里要用的fragment比较多,所以继承FragmentStatePagerAdapter,Why? ###内存优化 因为一个Fragment占的内存还是比较大,一旦fragment数量比较多了,后果你懂的。当页面不可见时, 对应的Fragment实例可能会被销毁,但是Fragment的状态会被保存,所以一些提高了我们的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的朋友应该懂,就是设置Tab的标题,因为我们的ViewPager是要与TabLayout进行关联的。

编写网络工具类 好了重头戏来了,也是本项目唯一的特色,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'

这里我不做过多关于RxJava和Retrofit的描述,因为相关资料网上一堆堆。RxJava说到底就是异步,这是它整个流程非常简洁明了,而且方便线程切换。Retrofit是讲OKHttp更好的封装下,简化我们的网络请求。 首先编写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(){
        //手动创建一个OkHttpClient并设置超时时间
        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时创建单例
    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;
    }
}

我们在外面只用调用getNews就行了,传一个Subscriber和类型就行了。逻辑都不是很复杂吧。里面的精髓就是线程切换 .subscribeOn(Schedulers.io()) ,RxJava给我吗提供了五个选择,这里因为我们是请求网络,所以就用io的,最后切换到主线程 .observeOn(AndroidSchedulers.mainThread())

编写Fragment

既然网络工具类写好了,那么就写个fragment来把这些数据展示出来吧!

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() {

            }
            //出现异常回调
            @Override
            public void onError(Throwable e) {
                Log.e("smile","获取失败");
            }
            //获取数据成功后回调
            @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()));
        //避免重复添加间距
        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());
    }
}

其实仔细一看也不是很复杂,就是调用我们开始写的getNews而已,请求成功后会回调onNext方法。对了这里有个转场动画,就是最后一个方法,这里的动画效果是共享元素,所以指定你要共享的元素就行,然后设置下它的 android:transitionName="photos"。

编写RecyclerView适配器 再见ListView,你好RecyclerView RecyclerView的优点就不多说了,就是自由,任性 适配器,我就不贴代码了,累,而且没什么特色。

编写详情页面 这里主要都是用了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就能实现了,还是简单的描述下那些属性的含义 CollapsingToolbarLayout

//折叠后的背景色  -> setContentScrim(Drawable)
  app:contentScrim="?attr/colorPrimary"   
  // 必须设置透明状态栏才有效  -> 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,主要是为了解决时间冲突导致滑动卡顿,就是不让拦截子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;
        }
    }

}

到这里差不多也要完工了,就可以看到开头的效果了,貌似第一次写这么长的博客,,, 最后附上源码 传送门 喜欢就给个星星吧!

About

采用RxJava + Retrofit

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages