一个小时打造新闻app

特别说明

  • 当前博客平台账号已废弃,如果有使用细节问题请前往我个人博客平台 HuRuWo的技术小站进行讨论交流。直接在其他平台留言可能无法及时得到回复。
  • 文章首发于个人博客HuRuWo的技术小站,如果本文非vip用户无法完全浏览或者图片无法打开,可前往个人博客文章地址查看文章并留言讨论。

  • 本篇文章个人博客文章地址一个小时打造新闻app,点击链接直接前往。

  • 更多技术文章访问本人博客HuRuWo的技术小站,包括 Electron从零开发 Android 逆向 app 微信数据抓取 抖音数据抓取 闲鱼数据抓取 小红书数据抓取 其他软件爬虫 等技术文章

前言

作为一个新手,学完基础总想做点什么东西出来。于是我试着去模仿那些优秀的开源作品。
模仿作品:LookLook开源项目
经过一些波折和学习,写下模仿过程。

一个小时打造新闻app

实际上我花了大概三天才弄懂所有的东西,不过有了经验确实可以在一个小时里完成。

使用框架:

rxjava和retrofit以及一个开源扩展的recyclerview和注解框架butterknife
集体依赖如下:

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support:design:24.2.1'
    testCompile 'junit:junit:4.12'
    //依赖注解
    //依赖添加
    compile 'com.jakewharton:butterknife:8.4.0'
    apt 'com.jakewharton:butterknife-compiler:8.4.0'
    compile 'com.google.code.gson:gson:2.7'
    //高级的recyclerview
    compile 'com.jude:easyrecyclerview:4.2.3'
    compile 'com.android.support:recyclerview-v7:24.2.0'
    //rxjava
    compile 'com.squareup.retrofit2:retrofit-converters:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-scalars:2.1.0'
    compile 'com.github.bumptech.glide:glide:3.7.0'
}

开始制作app

界面制作:

新建项目,选择模板---->调整模板


QQ截图20161025085115.png

菜单调整

可以看到有menu里面两个文件
一个是主菜单,显示在Toobar上面
另一个是抽屉的菜单,按需修改即可。
activity_main_drawer.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <group android:checkableBehavior="single">

        <item
            android:id="@+id/nav_camera"
            android:icon="@drawable/ic_menu_slideshow"
            android:title="新闻精选" />
        <item
            android:id="@+id/nav_gallery"
            android:icon="@drawable/ic_face_black_24dp"
            android:title="轻松一刻" />
        <item
            android:id="@+id/nav_slideshow"
            android:icon="@drawable/ic_menu_gallery"
            android:title="每日美图" />
        <item
            android:id="@+id/nav_manage"
            android:icon="@drawable/ic_menu_manage"
            android:title="应用推荐" />
    </group>

    <item android:title="其他">
        <menu>
            <item
                android:id="@+id/nav_share"
                android:icon="@drawable/ic_menu_share"
                android:title="软件分享" />
            <item
                android:id="@+id/nav_send"
                android:icon="@drawable/ic_menu_send"
                android:title="软件关于" />
        </menu>
    </item>

</menu>

抽屉除了menu还有上面一部分,可以设置头像和签名。
nav_header_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="@dimen/nav_header_height"
    android:background="@drawable/side_nav_bar"
    android:gravity="bottom"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:theme="@style/ThemeOverlay.AppCompat.Dark"
    android:orientation="vertical">

    <ImageView
        android:layout_gravity="center"
        android:id="@+id/imageView"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:srcCompat="@drawable/ic_app_icon" />

    <TextView
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="一日之计在于晨,一年之计在于春。"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

    <TextView
        android:gravity="center"
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="1458476478@qq.com" />

</LinearLayout>

主界面大概这就可以了,剩下的就是要动态添加fragement到FragLayout里面去。

数据获取

首先新建fragment_news,布局文件只需要一个EasyRecyclerView即可

添加依赖

//高级的recyclerview
    compile 'com.jude:easyrecyclerview:4.2.3'
    compile 'com.android.support:recyclerview-v7:24.2.0'
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">
    <com.jude.easyrecyclerview.EasyRecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:recyclerClipToPadding="true"
        app:recyclerPadding="8dp"
        app:recyclerPaddingBottom="8dp"
        app:recyclerPaddingLeft="8dp"
        app:recyclerPaddingRight="8dp"
        app:recyclerPaddingTop="8dp"
        app:scrollbarStyle="insideOverlay"
        app:scrollbars="none" />
</LinearLayout>

使用rxjava和retrofit获取json数据

我的数据来自天性数据,只需要注册即可获得APIKEY。
数据请求核心代码:

创建retrofit的请求接口

public interface ApiService{
    @GET("social/")
    Observable <NewsGson> getNewsData(@Query("key")String key,@Query("num") String num,@Query("page") int page);

注意返回的是Gson数据而且设置为"被观察者"

数据获取函数:

private void getData() {
        Log.d("page", page + "");
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://api.tianapi.com/")
                //String
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())//添加 json 转换器
                //    compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 适配器
                .build();
        ApiService apiManager = retrofit.create(ApiService.class);//这里采用的是Java的动态代理模式
        apiManager.getNewsData("你的APIKREY", "10", page)
                .subscribeOn(Schedulers.io())
                .map(new Func1<NewsGson, List<News>>() {
                    @Override
                    public List<News> call(NewsGson newsgson) { //
                        List<News> newsList = new ArrayList<News>();
                        for (NewsGson.NewslistBean newslistBean : newsgson.getNewslist()) {
                            News new1 = new News();
                            new1.setTitle(newslistBean.getTitle());
                            new1.setCtime(newslistBean.getCtime());
                            new1.setDescription(newslistBean.getDescription());
                            new1.setPicUrl(newslistBean.getPicUrl());
                            new1.setUrl(newslistBean.getUrl());
                            newsList.add(new1);
                        }
                        return newsList; // 返回类型
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<List<News>>() {
                    @Override
                    public void onNext(List<News> newsList) {
                        adapter.addAll(newsList);
                    }

                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onError(Throwable e) {
                        Toast.makeText(getContext(),
                                "网络连接失败", Toast.LENGTH_LONG).show();
                    }
                });
        page = page + 1;
    }
  1. 使用retrofit 发起网络请求
  2. 数据通过rxjava提交先在io线程里,返回到主线程
  3. 中间设置map 转换 把得到的Gson类转化为所需的News类(可以省略这一步)
  4. subscribe的onNext里处理返回的最终数据。

关于建立Gson类

Gson是谷歌的Json处理包,添加依赖。
compile 'com.google.code.gson:gson:2.7'
配合插件:GsonFormat可以快速通过json数据建立对应类。

my.gif

数据绑定到recyview

由于我们使用的是被扩展的recyview,所以用起来很方便。
具体使用去作者的githuaEasyRecyclerView

  1. Adapter
    继承recycle的adapter,主要返回自己的ViewHolder
public class NewsAdapter extends RecyclerArrayAdapter<News> {
    public NewsAdapter(Context context) {
        super(context);
    }

    @Override
    public BaseViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) {

        return new NewsViewHolder(parent);
    }
}
  1. ViewHolder
  public class NewsViewHolder extends BaseViewHolder<News> {
  private TextView mTv_name;
    private ImageView mImg_face;
    private TextView mTv_sign;

    public NewsViewHolder(ViewGroup parent) {
        super(parent,R.layout.news_recycler_item);
        mTv_name = $(R.id.person_name);
        mTv_sign = $(R.id.person_sign);
        mImg_face = $(R.id.person_face);    }

    @Override
    public void setData(final News data) {
        mTv_name.setText(data.getTitle());
        mTv_sign.setText(data.getCtime());
        Glide.with(getContext())
                .load(data.getPicUrl())
                .placeholder(R.mipmap.ic_launcher)
                .centerCrop()
                .into(mImg_face);
    }


}

3.设置recycleview

recyclerView.setAdapter(adapter = new NewsAdapter(getActivity()));
        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

        //添加边框
        SpaceDecoration itemDecoration = new SpaceDecoration((int) PixUtil.convertDpToPixel(8, getContext()));
        itemDecoration.setPaddingEdgeSide(true);
        itemDecoration.setPaddingStart(true);
        itemDecoration.setPaddingHeaderFooter(false);
        recyclerView.addItemDecoration(itemDecoration);

        //更多加载
        adapter.setMore(R.layout.view_more, new RecyclerArrayAdapter.OnMoreListener() {
            @Override
            public void onMoreShow() {
                getData();
            }

            @Override
            public void onMoreClick() {

            }
        });
        //写刷新事件
        recyclerView.setRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                recyclerView.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        adapter.clear();
                        page = 0;
                        getData();
                    }
                }, 1000);
            }
        });

        //点击事件
        adapter.setOnItemClickListener(new RecyclerArrayAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(int position) {
                ArrayList<String> data = new ArrayList<String>();
                data.add(adapter.getAllData().get(position).getPicUrl());
                data.add(adapter.getAllData().get(position).getUrl());
                Intent intent = new Intent(getActivity(), NewsDetailsActivity.class);
                //用Bundle携带数据
                Bundle bundle = new Bundle();
                bundle.putStringArrayList("data", data);
                intent.putExtras(bundle);
                startActivity(intent);
            }
        });

Glide网络图片加载库

一个专注于平滑图片加载的库:

依赖:
compile 'com.github.bumptech.glide:glide:3.7.0'

基本使用:

Glide.with(mContext)
                .load(path)
                .asGif()
                .override(300,300)
                .diskCacheStrategy(DiskCacheStrategy.SOURCE)
                .placeholder(R.drawable.progressbar)
                .thumbnail(1f)
                .error(R.drawable.error)
                .transform(new MyBitmapTransformation(mContext,10f))
                .into(iv);

新闻详情页

布局:使用CoordinatorLayout实现上拉toolbar压缩动画。

<?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.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="256dp"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:expandedTitleMarginEnd="64dp"
            app:expandedTitleMarginStart="48dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:src="@mipmap/ic_launcher"
                android:id="@+id/ivImage"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                android:scaleType="centerCrop"
                android:transitionName="新闻图片"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7" />

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <WebView
            android:id="@+id/web_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"></WebView>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

CoordinatorLayout+AppBarLayout里面配合CollapsingToolbarLayout布局技能实现toolbar的动画:

my.gif

上面的Imgview加载图片,下面的webview加载文章内容

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_news_detail);

        toolbar.setTitle("新闻详情");

        setSupportActionBar(toolbar);
//        设置返回箭头
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                onBackPressed();
            }
        });
        //新页面接收数据
        Bundle bundle = this.getIntent().getExtras();
        //接收name值
        final ArrayList<String> data = bundle.getStringArrayList("data");
        Log.d("url", data.get(0));

        webText.setWebViewClient(new WebViewClient() {

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // TODO Auto-generated method stub
                view.loadUrl(url);
                return true;
            }
        });
        webText.loadUrl(data.get(1));

        Glide.with(this)
                .load(data.get(0)).error(R.mipmap.ic_launcher)
                .fitCenter().into(ivImage);

    }

到这里基本完成:最后动态添加fragment

//菜单事件添加
if (id == R.id.nav_camera) {
            // Handle the camera action
            NewsFragment fragment=new NewsFragment();
            FragmentManager fragmentManager=getSupportFragmentManager();
            FragmentTransaction transaction=fragmentManager.beginTransaction();
            transaction.replace(R.id.fragment_container,fragment);
            transaction.commit();

        } 

效果测试:

my.gif
my.gif

总结:

只是勉强能用,还有很多细节没有优化。接下来好要继续学习。

补充:关于ButterKnife的使用

框架导入:
搜索依赖butterknife导入:

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.2.0'
    //依赖添加
    compile 'com.jakewharton:butterknife:8.4.0'
}

使用步骤:
注意我这里写的是8.40版本,和以前的有区别。
如果的ButterKnife是8.01或者以上的话
需要添加以下内容:
1.classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.2'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

2.apply plugin: 'com.neenbedankt.android-apt'

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

3.apt 'com.jakewharton:butterknife-compiler:8.4.0'

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.2.0'
    //依赖添加
    compile 'com.jakewharton:butterknife:8.4.0'
    apt 'com.jakewharton:butterknife-compiler:8.4.0'

}

Android Studio上方便使用butterknife注解框架的偷懒插件Android Butterknife Zelezny:

my.gif

技巧:鼠标要移动到布局文件名上。

文件已发至Github---https://github.com/HuRuWo/YiLan

关于接口调用失败,我测试发现天行数据失败。到后台发现原来是我的账号请求的次数超过上限了。(主要是大家用的都是我的账号,10000次请求分分钟就没了),所以各位可以自行申请账号。

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

推荐阅读更多精彩内容