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研发村 ),谢谢大家的支持。

Android实战:手把手实现“捧腹网”APP(三)-----UI实现,逻辑实现的更多相关文章

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

    Android实战:手把手实现"捧腹网"APP(一)-–捧腹网网页分析.数据获取 Android实战:手把手实现"捧腹网"APP(二)-–捧腹APP原型设计.实 ...

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

    Android实战:手把手实现"捧腹网"APP(一)-–捧腹网网页分析.数据获取 Android实战:手把手实现"捧腹网"APP(二)-–捧腹APP原型设计.实 ...

  3. Go语言之进阶篇爬捧腹网

    1.爬捧腹网 网页规律: https://www.pengfu.com/xiaohua_1.html   下一页 +1 https://www.pengfu.com/xiaohua_2.html 主页 ...

  4. python3制作捧腹网段子页爬虫

    0x01 春节闲着没事(是有多闲),就写了个简单的程序,来爬点笑话看,顺带记录下写程序的过程.第一次接触爬虫是看了这么一个帖子,一个逗逼,爬取煎蛋网上妹子的照片,简直不要太方便.于是乎就自己照猫画虎, ...

  5. py3+urllib+re,爬虫下载捧腹网图片

    实现原理及思路请参考我的另外几篇爬虫实践博客 py3+urllib+bs4+反爬,20+行代码教你爬取豆瓣妹子图:http://www.cnblogs.com/UncleYong/p/6892688. ...

  6. Go语言 之捧腹网爬虫案例

    package main import ( "fmt" "net/http" "os" "regexp" "s ...

  7. 项目- Vue全家桶实战去哪网App

    最近在学习Vue,花了几天时间跟着做了这个项目,算是对学习Vue入门的一个总结,欢迎同学们star 去哪网APP

  8. 【Android 应用开发】Android 开发环境下载地址 -- 百度网盘 adt-bundle android-studio sdk adt 下载

    19af543b068bdb7f27787c2bc69aba7f Additional Download (32-, 64-bit) Package r10 STL debug info androi ...

  9. Android开发全套视频教程在线观看网盘下载

    千锋金牌讲师老罗老师简介: 国内第一批Android教学讲师,10多年软件开发经验,6年多教学经验,曾担任广东电信北京分公司移动事业部项目经理,主持过微软中国平台考试系统.山西省旅游局智能化平台等大型 ...

随机推荐

  1. bootstrap-table的简要使用

    bttable功能强大!同时支持申明方式和编程方式来使用!是wenzhixin主导开发一款开源的表格控件! 文档比较详尽,但要求初学者已经比较熟悉js,jquey等基本内容,否则可能许多代码无法阅读! ...

  2. css3之文本和颜色功能之text-shadow

    总本看一下 1.text-shadow 语法:text-shadow: h-shadow v-shadow blur color; h-shadow: 必需.水平阴影的位置.允许负值. v-shado ...

  3. HTML5中类jQuery选择器querySelector和querySelectorAll的使用

    支持的浏览IE8+,Firefox3.5+,Safari3.1+ Chrome和Opera 10+ 1.querySelector()方法接收一个选择符,返回第一个匹配的第一个元素,如果没有返回nul ...

  4. MySQL系列(六)--索引优化

    在进行数据库查询的时候,索引是非常重要的,当然前提是达到一定的数据量.索引就像字典一样,通过偏旁部首来快速定位,而不是一页页 的慢慢找. 索引依赖存储引擎层实现,所以支持的索引类型和存储引擎相关,同一 ...

  5. 斐波那契字符串_KMP

    前言:通过这道题恶补了一下字符串匹配的知识 思路:首先就是求出菲波那切字符串,这个很简单,但是要注意递归超时的问题,可以考虑加上备忘录,或者用递推法,接下来就是匹配问题了,常规的BF会超时,所以要用K ...

  6. 火狐插件hostadmin

    windows的host文件:  c:\Windows\System32\drivers\etc\hosts 手动修改hosts文件费时又费力,可直接在FF附加组件中搜索hostadmin下载安装: ...

  7. mac 安装svn

    别人说用Xcode装,我也不知道我这个是不是用Xcode装的 在命令行界面输入 sudo bash svn --version 会出现一大段介绍,关于xcode的,我也不懂,一只敲空格键到最后,然后输 ...

  8. 实用Jupyter Notebook扩展工具——提升你的工作效率

    Jupyter Notebook 现已成为数据分析,机器学习的必备工具.因为它可以让数据分析师集中精力向用户解释整个分析过程.通过安装一些扩展工具,可以让你在Jupyter Notebook上的工作效 ...

  9. python 数据库风格的DataFrame合并

  10. Nginx 编译设置模块执行顺序

    Nginx编译时,配置"--add-module=xxx"可以加入模块,当我们需要按照指定顺序来设置过滤模块执行顺序时,先配置的"--add-module=xxx&quo ...