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. DSP日志打印 LOG_printf

    LOG_printf 依托BIOS环境,需要引用下列头文件: #include <std.h> #include <log.h>     并且,要在.tcf环境中添加一个LOG ...

  2. Dock镜像初探索

    一.安装CentOS版DockerCE 1.1 卸载旧的版本 yum remove docker \ docker-client \ docker-client-latest \ docker-com ...

  3. WPF 禁用中文

    <TextBox InputMethod.IsInputMethodEnabled="False" />

  4. $(...).live is not function

    项目中引入了一个插件,但是调用的时候就报了$(...).live is not function 上网搜索了一下live方法在1.9中被删除了,因为平时自己用的时候就用on的方法,没用过live,所以 ...

  5. 使用 windows 批处理指令(BAT文件)进行压缩文件(zip)解压操作

    以下指令包括文件删除.复制.zip文件解压操作.使用7z指令指令进行解压操作前,需要确保 windows 的 path 系统环境变量中存在7z的安装路径. 7z的下载地址:https://www.7- ...

  6. Redis源码解析:30发布和订阅

    Redis的发布与订阅功能,由SUBSCRIBE,PSUBSCRIBE,UNSUBSCRIBE,PUNSUBSCRIBE,以及PUBLISH等命令实现. 通过执行SUBSCRIBE命令,客户端可以订阅 ...

  7. Spring.Net2.0+NHibernate4.0 +Asp.Net Mvc4 一

    1.创建项目结构 控制器:    SN.Controllers 数据访问 :SN.Dao 实体映射: SN.Models 服务层:     SN.Servers 视图层:   SN.Web 2.添加需 ...

  8. JS 重载页面,本地刷新,返回上一页

    JS 重载页面,本地刷新,返回上一页 : <a href="javascript:history.go(-1)">返回上一页</a> <a href= ...

  9. git与github建立链接(学习笔记)

    总结步骤: 1.将所有文件添加到本库 git add . 2. git commit -m "提示信息随便写" 3.查看git修改状态 git status 4.获取远程库与本地同 ...

  10. mysql 无法存储joda time的datetime类型

    com.mysql.jdbc.MysqlDataTruncation: Data truncation: Incorrect datetime value: '\xAC\xED\x00\x05sr\x ...