Android实战:手把手实现“捧腹网”APP(一)—–捧腹网网页分析、数据获取

Android实战:手把手实现“捧腹网”APP(二)—–捧腹APP原型设计、实现框架选取

Android实战:手把手实现“捧腹网”APP(三)—–UI实现,逻辑实现


APP页面实现



根据原型图,我们可以看出,UI分为两部分,底部Tab导航+上方列表显示。 所以此处,我们通过 FragmentTabHost+Fragment,来实现底部的导航页面,通过RecyclerView来实现列表页面。

因为篇幅原因,关于FragmentTabHost和RecyclerView的使用,不多做介绍,可以建议参考: FragmentTabHost使用方法RecycleView_PullToRefresh_LoadMore两篇文章,其中后者关于Recyclerview的项目是我之前封装的一个支持下拉刷新,加载更多,添加Header和Footer等功能的RecyclerView,便于使用。

此处,再多说一点,因为是我们做自己来实现该ui,没美工给我设计图,切图标, 所以我们需要自己去找图标,此处推荐Iconfont-阿里巴巴矢量图标库, 在这里,我们可以找到很多的图标,选择适用的几个即可。

篇幅原因,具体的页面布局、实现代码,我这里就不多贴,有兴趣的,可以直接看源码,此处,只贴出列表list_item的页面布局代码。

从原型图中,我们可以看出,列表的显示分为纯文显示和图片显示,所以我们的item布局,应该要兼容这两种显示方式。

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:foreground="?android:attr/selectableItemBackground"
card_view:cardBackgroundColor="#FFFFFF"
card_view:cardCornerRadius="8dp"
card_view:cardElevation="2dp"
card_view:cardUseCompatPadding="true"> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"> <RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="6dp"
android:gravity="center_vertical"> <com.lnyp.joke.widget.CircleImageView
android:id="@+id/imgUser"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/ic_launcher" /> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="14dp"
android:layout_toRightOf="@id/imgUser"
android:orientation="vertical"> <TextView
android:id="@+id/textUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="textUserNametextUserNametextUserNametextUserNametextUserNametextUserNametextUserNametextUserNametextUserName"
android:textColor="#333333"
android:textSize="14sp" /> <TextView
android:id="@+id/textLastTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_toRightOf="@id/imgUser"
android:gravity="right"
android:singleLine="true"
android:text="textLastTime"
android:textColor="#555555"
android:textSize="12sp" /> </LinearLayout> </RelativeLayout> <TextView
android:id="@+id/textContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="4dp"
android:gravity="left"
android:lineSpacingExtra="4dp"
android:text="描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述"
android:textColor="#333333"
android:textSize="15sp"
android:visibility="gone" /> <com.lnyp.joke.widget.ShowMaxImageView
android:id="@+id/imgJoke"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
android:src="@mipmap/ic_launcher" /> <TextView
android:id="@+id/textTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="4dp"
android:singleLine="true"
android:text="title title title tiltle title"
android:textColor="#333333"
android:textSize="16sp" /> <LinearLayout
android:id="@+id/layoutTags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="4dp"
android:gravity="right"
android:orientation="horizontal"> <TextView
android:id="@+id/textTag1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginLeft="4dp"
android:background="@drawable/house_tag_text_bg"
android:gravity="left"
android:paddingBottom="2dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="2dp"
android:singleLine="true"
android:text="标签"
android:textColor="#333333"
android:textSize="10sp" /> <TextView
android:id="@+id/textTag2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginLeft="4dp"
android:background="@drawable/house_tag_text_bg"
android:gravity="left"
android:paddingBottom="2dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="2dp"
android:singleLine="true"
android:text="标签"
android:textColor="#333333"
android:textSize="10sp" /> <TextView
android:id="@+id/textTag3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginLeft="4dp"
android:background="@drawable/house_tag_text_bg"
android:gravity="left"
android:paddingBottom="2dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="2dp"
android:singleLine="true"
android:text="标签"
android:textColor="#333333"
android:textSize="10sp" /> <TextView
android:id="@+id/textTag4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginLeft="4dp" android:background="@drawable/house_tag_text_bg"
android:gravity="left"
android:paddingBottom="2dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="2dp"
android:singleLine="true"
android:text="标签"
android:textColor="#333333"
android:textSize="10sp" />
</LinearLayout> </LinearLayout> </android.support.v7.widget.CardView>

效果图:

我们只需要在实现的逻辑上,控制文字、图片的显隐就好了。

APP逻辑功能实现

1.数据获取,实现列表适配器

在第一章捧腹网网页分析、数据获取中,我已经讲过了如何去解析网页中的数据为我们所用,拿到数据后,我们需要用这些数据填充RecyclerView,此处,使用的是我已经封装好的RecyclerView,支持翻页加载数据。

import android.support.v4.app.Fragment;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.lnyp.joke.R;
import com.lnyp.joke.pengfu.JokeBean;
import com.lnyp.joke.widget.CircleImageView;
import com.lnyp.joke.widget.ShowMaxImageView; import java.util.List; import butterknife.BindView;
import butterknife.ButterKnife; /**
*笑话列表
*/
public class JokeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private LayoutInflater mInflater; private Fragment mContext; private List<JokeBean> mDatas; private View.OnClickListener onItemClick; private int screenWidth; public JokeListAdapter(Fragment context, List<JokeBean> datas, View.OnClickListener onItemClick) { this.mContext = context; this.mDatas = datas; this.onItemClick = onItemClick; mInflater = LayoutInflater.from(context.getActivity()); DisplayMetrics metric = new DisplayMetrics();
context.getActivity().getWindowManager().getDefaultDisplay().getMetrics(metric);
screenWidth = metric.widthPixels;
} @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = mInflater.inflate(R.layout.list_item_joke, parent, false); return new ViewHolder(view);
} @Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { ViewHolder viewHolder = (ViewHolder) holder; JokeBean jokeBean = mDatas.get(position); if (jokeBean != null) { Glide.with(mContext)
.load(jokeBean.getUserAvatar())
.asBitmap()
.centerCrop()
.into(viewHolder.imgUser); viewHolder.textUserName.setText(jokeBean.getUserName());
viewHolder.textLastTime.setText(jokeBean.getLastTime());
viewHolder.textTitle.setText(jokeBean.getTitle()); JokeBean.DataBean dataBean = jokeBean.getDataBean();
if (dataBean != null) {
if (TextUtils.isEmpty(dataBean.getContent())) { viewHolder.textContent.setVisibility(View.GONE);
viewHolder.imgJoke.setVisibility(View.VISIBLE);
viewHolder.textTitle.setVisibility(View.VISIBLE); // System.out.println(dataBean.getShowImg() + " " + dataBean.getGifsrcImg()); double width = Double.parseDouble(dataBean.getWidth());
double height = Double.parseDouble(dataBean.getHeight());
ViewGroup.LayoutParams lp = viewHolder.imgJoke.getLayoutParams();
lp.width = (int) (screenWidth * 0.8);
lp.height = (int) (screenWidth * 0.8 * height / width);
viewHolder.imgJoke.setLayoutParams(lp); String url = dataBean.getShowImg();
String gifUrl = dataBean.getGifsrcImg();
System.out.println("url : " + url + " gifUrl : " + gifUrl);
if (TextUtils.isEmpty(gifUrl)) {
Glide.with(mContext).load(url).asBitmap().diskCacheStrategy(DiskCacheStrategy.SOURCE).into(viewHolder.imgJoke);
} else {
Glide.with(mContext).load(gifUrl).asGif().diskCacheStrategy(DiskCacheStrategy.SOURCE).into(viewHolder.imgJoke);
} } else {
viewHolder.textContent.setVisibility(View.VISIBLE);
viewHolder.imgJoke.setVisibility(View.GONE);
viewHolder.textTitle.setVisibility(View.GONE); viewHolder.textContent.setText(dataBean.getContent());
}
} List<String> tags = jokeBean.getTags();
if (tags != null) { int size = tags.size();
if (size == 0) {
updateTags(viewHolder, View.GONE, View.GONE, View.GONE, View.GONE);
} else if (size == 1) {
viewHolder.textTag1.setText(tags.get(0));
updateTags(viewHolder, View.VISIBLE, View.GONE, View.GONE, View.GONE);
} else if (size == 2) {
viewHolder.textTag1.setText(tags.get(0));
viewHolder.textTag2.setText(tags.get(1));
updateTags(viewHolder, View.VISIBLE, View.VISIBLE, View.GONE, View.GONE);
} else if (size == 3) {
viewHolder.textTag1.setText(tags.get(0));
viewHolder.textTag2.setText(tags.get(1));
viewHolder.textTag3.setText(tags.get(2));
updateTags(viewHolder, View.VISIBLE, View.VISIBLE, View.VISIBLE, View.GONE);
} else {
viewHolder.textTag1.setText(tags.get(0));
viewHolder.textTag2.setText(tags.get(1));
viewHolder.textTag3.setText(tags.get(2));
viewHolder.textTag4.setText(tags.get(3));
updateTags(viewHolder, View.VISIBLE, View.VISIBLE, View.VISIBLE, View.VISIBLE);
}
viewHolder.layoutTags.setVisibility(View.VISIBLE);
} else {
updateTags(viewHolder, View.GONE, View.GONE, View.GONE, View.GONE);
viewHolder.layoutTags.setVisibility(View.GONE);
} viewHolder.imgJoke.setTag(R.string.app_name, position);
viewHolder.imgJoke.setOnClickListener(onItemClick);
}
} private void updateTags(ViewHolder viewHolder, int v1, int v2, int v3, int v4) {
viewHolder.textTag1.setVisibility(v1);
viewHolder.textTag2.setVisibility(v2);
viewHolder.textTag3.setVisibility(v3);
viewHolder.textTag4.setVisibility(v4);
} @Override
public int getItemCount() { return mDatas != null ? mDatas.size() : 0;
} class ViewHolder extends RecyclerView.ViewHolder { @BindView(R.id.imgJoke)
public ShowMaxImageView imgJoke; @BindView(R.id.textContent)
public TextView textContent; @BindView(R.id.layoutTags)
public LinearLayout layoutTags; @BindView(R.id.textTitle)
public TextView textTitle; @BindView(R.id.textTag1)
public TextView textTag1; @BindView(R.id.textTag2)
public TextView textTag2; @BindView(R.id.textTag3)
public TextView textTag3; @BindView(R.id.textTag4)
public TextView textTag4; @BindView(R.id.imgUser)
public CircleImageView imgUser; @BindView(R.id.textUserName)
public TextView textUserName; @BindView(R.id.textLastTime)
public TextView textLastTime; public ViewHolder(View itemView) {
super(itemView); ButterKnife.bind(this, itemView);
}
}
}

对于RecyclerView的适配器RecyclerView.Adapter的使用方式,相信玩过它的人都很熟悉,里面的方法不多介绍,主要讲下图片处理这块的实现:

String url = dataBean.getShowImg();
String gifUrl = dataBean.getGifsrcImg();
System.out.println("url : " + url + " gifUrl : " + gifUrl);
if (TextUtils.isEmpty(gifUrl)) {
Glide.with(mContext).load(url).asBitmap().diskCacheStrategy(DiskCacheStrategy.SOURCE).into(viewHolder.imgJoke);
} else {
Glide.with(mContext).load(gifUrl).asGif().diskCacheStrategy(DiskCacheStrategy.SOURCE).into(viewHolder.imgJoke);
}

app中要展示的图片,分为静态图片和动态图片,glide可以很好的处理gif动态图的加载,但是,如果像下面这样直接使用glide加载动态图,效率可是比较慢的。

Glide.with(mContext).load(gifUrl)into(viewHolder.imgJoke);

所以,在加载gif动态图的时候,我们通常使用下面这样的缓存策略

Glide.with(mContext).load(gifUrl).asGif().diskCacheStrategy(DiskCacheStrategy.SOURCE).into(viewHolder.imgJoke);

除了图片加载,还有一点需要讲解下,就是下面这段代码:

double width = Double.parseDouble(dataBean.getWidth());
double height = Double.parseDouble(dataBean.getHeight());
ViewGroup.LayoutParams lp = viewHolder.imgJoke.getLayoutParams();
lp.width = (int) (screenWidth * 0.8);
lp.height = (int) (screenWidth * 0.8 * height / width);
viewHolder.imgJoke.setLayoutParams(lp);

这段代码的意思是,在加载图片之前,先设置了ImageView的宽高。这样做的目的,是为了在图片加载显示之前就固定ImageView的大小,避免了列表因为图片高度不一致而出现“晃动”。

好,到这里,我们基本完成了列表显示功能了,接下来,在Fragment中实现功能逻辑。

2.实现列表逻辑功能

package com.lnyp.joke.fragment;

import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import com.baoyz.widget.PullRefreshLayout;
import com.lnyp.flexibledivider.HorizontalDividerItemDecoration;
import com.lnyp.joke.R;
import com.lnyp.joke.adapter.JokeListAdapter;
import com.lnyp.joke.http.HttpUtils;
import com.lnyp.joke.pengfu.JokeApi;
import com.lnyp.joke.pengfu.JokeBean;
import com.lnyp.joke.pengfu.JokeUtil;
import com.lnyp.joke.widget.SmartisanDrawable;
import com.lnyp.recyclerview.EndlessRecyclerOnScrollListener;
import com.lnyp.recyclerview.HeaderAndFooterRecyclerViewAdapter;
import com.lnyp.recyclerview.RecyclerViewLoadingFooter;
import com.lnyp.recyclerview.RecyclerViewStateUtils;
import com.victor.loading.rotate.RotateLoading; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import java.util.ArrayList;
import java.util.List; import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder; public class MainFragment extends Fragment { private Unbinder unbinder; @BindView(R.id.rotateloading)
public RotateLoading rotateloading; @BindView(R.id.swipeRefreshLayout)
public PullRefreshLayout swipeRefreshLayout; @BindView(R.id.listInspirations)
public RecyclerView listInspirations; private HeaderAndFooterRecyclerViewAdapter mAdapter; private List<JokeBean> mDatas; private int page = 1; private boolean mHasMore = false; private boolean isRefresh = true; // 处理请求返回信息
private MyHandler mHandler = new MyHandler(); private class MyHandler extends Handler { public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 0: RecyclerViewStateUtils.setFooterViewState(listInspirations, RecyclerViewLoadingFooter.State.Normal);
swipeRefreshLayout.setRefreshing(false);
rotateloading.stop(); mAdapter.notifyDataSetChanged(); break;
}
}
} public MainFragment() {
} @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_main, container, false); unbinder = ButterKnife.bind(this, view); initView(); rotateloading.start();
refreshReq(); return view;
} private void initView() { mDatas = new ArrayList<>(); JokeListAdapter jokeListAdapter = new JokeListAdapter(this, mDatas, onClickListener);
mAdapter = new HeaderAndFooterRecyclerViewAdapter(jokeListAdapter);
listInspirations.setAdapter(mAdapter); listInspirations.setLayoutManager(new LinearLayoutManager(getActivity()));
listInspirations.addItemDecoration(
new HorizontalDividerItemDecoration.Builder(getActivity())
.colorResId(R.color.divider_color)
.build()); listInspirations.addOnScrollListener(mOnScrollListener); swipeRefreshLayout.setOnRefreshListener(onRefreshListener); swipeRefreshLayout.setRefreshDrawable(new SmartisanDrawable(getActivity(), swipeRefreshLayout));
swipeRefreshLayout.setBackgroundColor(Color.parseColor("#EFEFEF"));
swipeRefreshLayout.setColor(Color.parseColor("#8F8F81")); } private PullRefreshLayout.OnRefreshListener onRefreshListener = new PullRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
refreshReq();
}
}; private void refreshReq() { isRefresh = true; page = 1; qryJokes();
} private void qryJokes() { final String url = JokeApi.PENGFU_NEW_JOKES + page + JokeApi.URL_SUFFIX;
System.out.println(url); HttpUtils.doGetAsyn(url, new HttpUtils.CallBack() { @Override
public void onRequestComplete(String result) { if (result == null) {
return;
}
System.out.println(result); Document doc = Jsoup.parse(result); if (doc != null) { JokeUtil jokeUtil = new JokeUtil();
List<JokeBean> jokeBeens = jokeUtil.getNewJokelist(doc); if (jokeBeens != null) { page++;
mHasMore = true; if (isRefresh) {
mDatas.clear();
isRefresh = false;
} mDatas.addAll(jokeBeens); mHandler.sendEmptyMessage(0); }
}
}
}); } private EndlessRecyclerOnScrollListener mOnScrollListener = new EndlessRecyclerOnScrollListener() {
@Override
public void onLoadNextPage(View view) {
super.onLoadNextPage(view); RecyclerViewLoadingFooter.State state = RecyclerViewStateUtils.getFooterViewState(listInspirations); if (state == RecyclerViewLoadingFooter.State.Loading) {
return;
} if (mHasMore) {
RecyclerViewStateUtils.setFooterViewState(getActivity(), listInspirations, mHasMore, RecyclerViewLoadingFooter.State.Loading, null);
qryJokes(); } else {
RecyclerViewStateUtils.setFooterViewState(getActivity(), listInspirations, mHasMore, RecyclerViewLoadingFooter.State.TheEnd, null);
}
}
}; private View.OnClickListener onClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
int pos = (int) view.getTag(R.string.app_name);
JokeBean jokeBean = mDatas.get(pos);
String showImg = jokeBean.getDataBean().getShowImg();
String gifSrcImg = jokeBean.getDataBean().getGifsrcImg();
//
System.out.println(showImg + " " + gifSrcImg);
} catch (Exception e) {
e.printStackTrace();
} }
}; @Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
}

功能逻辑比较简单,不多做解释。 到这里,我们的“捧腹”APP已经完成了80%了。下面,在做些扩展性的功能,使得它更像一个完整的APP。

3. 图片的大图浏览功能

上篇博文,我们就提到了,我们要使用PhotoView实现大图的浏览功能。

PhotoView的使用,可以直接在它的github官方介绍上看到,下面,我直接贴出使用代码。


import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView; import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import butterknife.BindView;
import butterknife.ButterKnife;
import uk.co.senab.photoview.PhotoViewAttacher; /**
* 图片浏览
*/
public class PhotoActivity extends FragmentActivity { @BindView(R.id.imgJoke)
public ImageView imgJoke; private String showImg;
private String gifSrcImg; private PhotoViewAttacher mAttacher; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photo); ButterKnife.bind(this); mAttacher = new PhotoViewAttacher(imgJoke); showImg = getIntent().getStringExtra("showImg");
gifSrcImg = getIntent().getStringExtra("gifSrcImg");
System.out.println(showImg + " " + gifSrcImg); if (TextUtils.isEmpty(gifSrcImg)) {
Glide.with(this).load(showImg).asBitmap().diskCacheStrategy(DiskCacheStrategy.SOURCE).into(imgJoke);
} else {
Glide.with(this).load(gifSrcImg).asGif().diskCacheStrategy(DiskCacheStrategy.SOURCE).into(imgJoke);
} mAttacher.update(); mAttacher.setOnPhotoTapListener(new PhotoViewAttacher.OnPhotoTapListener() {
@Override
public void onPhotoTap(View view, float x, float y) {
PhotoActivity.this.finish();
} @Override
public void onOutsidePhotoTap() {
PhotoActivity.this.finish();
}
});
}
}

程序中,我们添加了一个事件监听,主要是为了在用户单击图片显示或者不显示部分时,可以退出浏览。

4.为APP加入Bughd 实现崩溃分析、版本更新功能

功能做到这里,基本上完成了90%的APP,接下来,我们为其加入崩溃分析、版本更新功能。

关于如何配置,大家直接到http://bughd.com/doc/android官网看,看官方文档,为app添加功能,是开发的基本能力,而且,这个功能集成并不困难,建议大家自己添加,有疑问,可参考我最后放出的源码。

APP打包发布

截止此处,我们的“捧腹”APP基本上就已经实现了,在说打包发布之前,我要提到一个很重要的问题,那就是数据版权。 我们知道,这个app的数据,是分析“捧腹网”的网页,拿到的,我们应当尊重其版权所有。 因为我们是学习使用,所以大家应在app明显的位置,加上数据来源。这里,我选择在启动页面上添加。

接下来,就是打包发布了。关于如何打包app,限于篇幅,请参考我之前写的Android Studio(十二):打包多个发布渠道的apk文件 ,打包apk成功后,我们将其发布在fir.im免费托管分发服务的平台上,方便大家下载测试。(如果没问题,可以上传到应用市场)。





最后,让我们看下APP最终效果。

项目小结:

如果你能耐下心来,看完这三篇实战博文,相信你也可以做一个简单的app了。这个app实现并不难,博文讲解的也算是详尽,很容易理解。

这系列的博文,主要是针对初中级开发者,帮大家在研发过程中,理清思路,一步步完成一个完整的app。希望看完这篇博文的朋友,也能够举一反三, 做出一个自己所属的app。

源码地址:https://github.com/zuiwuyuan/Joke

apk下载地址: http://fir.im/xiaohane

欢迎有问题的朋友,留言讨论,也欢迎进QQ群来讨论交流:487786925( Android研发村 ),谢谢大家的支持。

最新文章

  1. day3 字典,集合,文件
  2. Java程序设计 实验二 Java面向对象程序设计
  3. SQL SERVER 与ACCESS、EXCEL的数据转换
  4. whois配置
  5. ASP.NET分页
  6. WPF简单的口算案例
  7. Linux-PAM(Linux下的密碼認證和安全机制)系統管理員指南(中文版)
  8. 【转】C#.net拖拽实现获得文件路径
  9. java中File类的相关学习
  10. [转] windows 上用程序putty使用 ssh自动登录Linux(Ubuntu)
  11. Intellj IDEA常用快捷键
  12. Squid快速入门(yum安装)
  13. BZOJ 400题纪念
  14. 【洛谷P3469】BLO
  15. vue事件处理器
  16. vs2017企业版本安装和序列号
  17. openlayers空间点查询之GetFeatureInfo
  18. html部分常用标签的含义及作用
  19. RAMPS1.4 3d打印控制板接线与测试4
  20. Asp.Net IIS7.5伪静态设置

热门文章

  1. https://github.com/ronggang/transmission-web-control
  2. 《DSP using MATLAB》Problem 7.30
  3. C#通过鼠标点击panel移动来控制无边框窗体移动
  4. PHP基于openssl实现的非对称加密操作
  5. python-基础-基础知识-变量-选择-循环
  6. log4j 配置文件参数说明
  7. Redis源码解析:18Hiredis同步API和回复解析API代码解析
  8. 洛谷P5071 此时此刻的光辉
  9. 对比Model前后数据保存不同值
  10. Lvs 环境搭建 vbox搭建centos6.9 vbox设置 centos上安装nginx