前言
從零開始,手把手帶你實現一個「專注睡前的 APP」。睡覺之前如果能有一個 APP,能讓我們寫一寫這一天的見聞或者心得,同時又能看一會段子、瞄一會好看的妹子,放鬆一下疲憊的身心那該多好,這也是我完成這個 APP 的原因。APP 的全部程式碼我已經分享到 Github 上了,需要的直接 點選這裡,歡迎 star 和 fork.
在開始寫正文之前,先來一波效果的展示,看看五天過後我們能實現怎樣的效果
本次的教程分為 5 天,內容分別為:
Day one,準備
- 功能需求
- 可行性分析
Day two,UI 及公共類的封裝
- 介面的設計及實現
- 公共類的實現
- Day three,日記模組
- 日記的展示
- 懸浮選單的實現
- 日記增刪改的實現
- Day four,妹子模組
- 圖片的獲取
- 圖片的展示
- 詳情頁面的展示
- Day five,段子模組
- 段子資料的獲取
- 段子的顯示
Day one
俗話說,萬事開頭難,在開始敲程式碼之前,先讓我們來做一些必要的準備,這樣才能事半功倍嘛!
一、功能需求
既然要做一個 APP,那我們首先還是得把 APP 的功能都列出來,有了方向才能更好的努力,因為我想做的是一個專門給睡覺前用的 APP,所以我覺得應該有以下的這些功能
- 1、日記的增刪改
- 2、顯示一些有趣好玩的段子
- 3、瀑布流展示漂亮的妹子
- 4、儲存日記的內容以及快取妹子圖片
雖然說需求不多,但是卻要運用到網路、資料儲存、圖片快取、UI 設計等內容,相信整個 APP 完成下來,必定能鞏固我們的 Android 基礎。
二、可行性分析
我們這個 APP 主要有三個模組,日記模組主要是運用到了資料庫的知識,難度不大。但是,段子模組和妹子模組的資料要從哪來,這便是要好好考慮的了。幸好現在是個開源的時代,很多的資料,網上已經開源出來了。
我們先來看一下資料的內容
group: {
text: "教授在河邊,常常看到兩隻龜,縮著一動不動。有天忍不住好奇,問一農
民:這兩隻烏龜在幹嗎?農民說:他們在pk。教授不解地問:動都沒動過p什麼
k。老農說:他們在比誰壽命長。教授說:可是殼上有甲骨文的那隻,早就死了埃
這時,另一隻猛然探出頭來罵到:md,死了也不吭一聲!有甲骨文的那隻也伸
出頭來:“專家說啥你信啥1",
user: {
user_id: 4669064575,
name: "饅頭啊",
avatar_url: "http://p3.pstatp.com/medium/6237/7969345239",
},
content: "教授在河邊,常常看到兩隻龜,縮著一動不動。有天忍不住好奇,問
一農民:這兩隻烏龜在幹嗎?農民說:他們在pk。教授不解地問:動都沒動過
p什麼k。老農說:他們在比誰壽命長。教授說:可是殼上有甲骨文的那隻,早
就死了埃這時,另一隻猛然探出頭來罵到:md,死了也不吭一聲!有甲骨文
的那隻也伸出頭來:“專家說啥你信啥1",
...
}複製程式碼
{
id: "56cc6d1d421aa95caa7076df",
type: "福利",
url: "http://ww1.sinaimg.cn/large/7a8aed7bgw1esxxi1vbq0j20qo0hstcu.jpg",
used: true,
who: "張涵宇"
}複製程式碼
上面那兩段程式碼分別是段子和妹子模組的 json 型別的資料,我已經將一些沒用的欄位去掉了。剩下的都是我們想要的資料。可以看到段子資料中,有著段子的內容,以及釋出者的頭像和名字。而妹子資料中有著圖片的 url、id、以及圖片的型別。相信有了這麼豐富的資料,我們想要完成這個 APP 也是有底氣了。
Day two
一、介面的設計及實現
既然我們想要完成一個好看的 APP,那麼好看的介面便是必不可少的,這裡我強烈推薦 APP 介面的設計必須儘量遵從 Google 提出的 Material Design,在這個推薦一個能夠讓我們實現 Material Design 變得更加簡單的網站 material design palette,我這個 APP 的配色就是用這個網站完成的,貼幾張圖片,讓你感受一下它的強大
藉助這個網站便能讓我們完成 APP 的配色以及圖示的收集,為下一步功能的實現,先打好了基礎,至於介面的設計就仁者見仁智者見智了,篇幅有限,我就不多講了。
APP 的最終設計效果如下:
二、公共類的實現
因為這個專案有三個模組,有一些東西其實是可以通用的,如果我們先把這些能夠通用的東西,封裝起來,供給所有的模組呼叫的話,相信會大大提高我們的開發效率。
1、網路工具類的封裝
這個 APP 中,很多地方都要用到網路請求,因此也就很有必要將網路請求封裝起來,因為這個 APP 的規模比較小,因此我選擇了 Volley 這個網路框架作為我們網路請求庫,把網路請求封裝起來,哪個地方需要,呼叫一下就行了。對於網路請求,我覺得每個程式設計師都該懂點 HTTP,這裡附上一篇有關 HTTP 的文章 程式設計師都該懂點 HTTP。
先讓我們來寫個將網路請求進行回撥的介面
public interface VolleyResponseCallback {
void onSuccess(String response);
void onError(VolleyError error);
}複製程式碼
然後將網路請求封裝起來
public class VolleyHelper {
/**
* 用於傳送 Get 請求的封裝方法
*
* @param context Activity 的例項
* @param url 請求的地址
* @param callback 用於網路回撥的介面
*/
public static void sendHttpGet(Context context, String url, final VolleyResponseCallback callback){
RequestQueue requestQueue = Volley.newRequestQueue(context);
StringRequest stringRequest = new StringRequest(url
, new Response.Listener<String>() {
@Override
public void onResponse(String s) {
callback.onSuccess(s);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
callback.onError(error);
}
});
requestQueue.add(stringRequest);
}
}複製程式碼
2、Json 解析的幫助類
因為我們這個 APP 中,獲取到的資料都是 Json 格式的,因此也就有必要將有關的 Json 解析封裝成一個工具類,傳入一個 String 型別的資料,直接得到資料實體類的 List。
public class CommonParser {
/**
* 用來解析列表性的JSON資料
* 如:
* {"success":true,"fileList":[{"filename":"檔名1","fileSize":"檔案大小1"},
* {"filename":"檔名2","fileSize":"檔案大小2"}]}
*
* @param result 網路返回來的JSON資料 比如:上面的整串資料
* @param successKey 判斷網路是否成功的欄位 比如:上面的success欄位
* @param arrKey 列表的欄位 比如:上面的fileList欄位
* @param clazz 需要解析成的Bean型別
* @param <T> 需要解析成的Bean型別
* @return
*/
public static <T> List<T> parseForList(String result, String successKey, String arrKey, Class<T> clazz) {
List<T> list = new ArrayList<>();
JSONObject rootJsonObject = null;
try {
rootJsonObject = new JSONObject(result);
if (rootJsonObject.getBoolean(successKey)) {
JSONArray rootJsonArray = rootJsonObject.getJSONArray(arrKey);
Gson g = new Gson();
for (int i = 0; i < rootJsonArray.length(); i++) {
T t = g.fromJson(rootJsonArray.getJSONObject(i).toString(), clazz);
list.add(t);
}
}
} catch (JSONException e) {
e.printStackTrace();
}
return list;
}
}複製程式碼
3、HomeActivity(主頁面)的封裝
主頁面我用的是 TabLayout + ViewPager + Fragment,也是現在主流 APP 主頁面的顯示方式。主介面底部是我們三個模組的圖示和名稱,通過左右滑動能實現介面的跳轉。
底部圖示的實體類 CommonTabBean
public class CommonTabBean implements CustomTabEntity{
private int selectedIcon;
private int unselectedIcon;
private String title;
public CommonTabBean(String title){
this.title = title;
}
public CommonTabBean(String title, int selectedIcon, int unselectedIcon) {
this.title = title;
this.selectedIcon = selectedIcon;
this.unselectedIcon = unselectedIcon;
}
@Override
public String getTabTitle() {
return title;
}
@Override
public int getTabSelectedIcon() {
return selectedIcon;
}
@Override
public int getTabUnselectedIcon() {
return unselectedIcon;
}
}複製程式碼
ViewPager + Fragment 通用的 Adapter
public class CommonPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> mFragments;
public CommonPagerAdapter(FragmentManager fragmentManager, List<Fragment> mFragments){
super(fragmentManager);
this.mFragments = mFragments;
}
@Override
public Fragment getItem(int position) {
return mFragments.get(position);
}
@Override
public int getCount() {
return mFragments.size();
}
}複製程式碼
Day three
關於日記模組的實現,其實我是複用了以前寫過的一個日記 APP,具體的思路和做法,可以參考我的這篇文章 Android 一款十分簡潔、優雅的日記 APP
Day four
一、圖片的獲取
1、根據返回的資料來編寫圖片的實體類
public class MeiziBean {
@SerializedName("_id")
private String id;
@SerializedName("url")
private String imageUrl;
@SerializedName("who")
private String who;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getImageUrl() {
return imageUrl;
}
public MeiziBean(String imageUrl){
this.imageUrl = imageUrl;
}
}複製程式碼
2、圖片的展示
可以看到我是用瀑布流的方式來實現圖片的展示,效果還不錯,但其實實現起來也是很簡單的
先寫個圖片的佈局作為 RecyclerView 的 Item
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/item_iv_meizi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
/>
</android.support.v7.widget.CardView>複製程式碼
可以看到我在 ImageView 的外面加了一個 CardView,這個一種卡片式佈局,能讓圖片看起來就像一張卡片一樣,相當的優雅、美觀。
接著編寫 Adapter,將資料和介面進行繫結
public class MeiziAdapter extends RecyclerView.Adapter<MeiziAdapter.MeiziViewHolder> {
private List<MeiziBean> mMeiziBeanList;
private Fragment mFragment;
public MeiziAdapter(List<MeiziBean> mMeiziBeanList, Fragment mFragment){
this.mMeiziBeanList = mMeiziBeanList;
this.mFragment = mFragment;
}
@Override
public MeiziViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_meizi, null);
return new MeiziViewHolder(view);
}
@Override
public void onBindViewHolder(MeiziViewHolder holder, final int position) {
Glide.with(mFragment)
.load(mMeiziBeanList.get(position).getImageUrl())
.fitCenter()
.dontAnimate()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(holder.mIvMeizi);
holder.mIvMeizi.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ArrayList<String> resultList = new ArrayList<String>();
for (MeiziBean meiziBean : mMeiziBeanList) {
resultList.add(meiziBean.getImageUrl());
}
DetailActivity.startActivity(mFragment.getActivity(), resultList, position);
}
});
}
@Override
public int getItemCount() {
if(mMeiziBeanList.size() > 0){
return mMeiziBeanList.size();
}
return 0;
}
public static class MeiziViewHolder extends RecyclerView.ViewHolder{
ImageView mIvMeizi;
public MeiziViewHolder(View itemView) {
super(itemView);
mIvMeizi = (ImageView) itemView.findViewById(R.id.item_iv_meizi);
}
}
}複製程式碼
最後在 Fragment 進行資料的獲取,以及佈局的初始化就行了
public class MeiziFragment extends Fragment {
......
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_meizi, container, false);
ButterKnife.bind(this, view);
initView();
refreshMeizi();
return view;
}
/**
* 重新整理當前介面
*/
private void refreshMeizi() {
mRefresh.setColorSchemeResources(R.color.colorPrimary);
mRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
initView();
mRefresh.setRefreshing(false);
}
});
}
private void initView() {
VolleyHelper.sendHttpGet(getActivity(), MeiziApi.getMeiziApi(), new VolleyResponseCallback() {
@Override
public void onSuccess(String s) {
response = s;
meiziBeanList = GsonHelper.getMeiziBean(response);
mRvShowMeizi.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
Collections.shuffle(meiziBeanList);
mRvShowMeizi.setAdapter(new MeiziAdapter(meiziBeanList, MeiziFragment.this));
}
@Override
public void onError(VolleyError error) {
Logger.d(error);
}
});
}複製程式碼
3、詳情頁面的展示
乾巴巴的,整個模組只能顯示妹子的圖片怎麼行呢!!!怎麼著也得能檢視大圖,根據手勢放大縮小,以及瀏覽下一張圖片才行嘛,說幹就幹。
因為圖片需要有根據手勢來放大縮小的功能,因此我便想到了 PhotoView,這是網上一個大神寫的,繼承自 ImageView 的一個自定義控制元件。圖片載入我用的是
Glide,如果沒了解過這個庫的,強烈推薦,一行程式碼就能搞定圖片載入,你確定不研究一下。這裡附上一篇有關 Glide 的文章 Glide 一個強大的圖片載入框架
public class DetailFragment extends Fragment {
public static DetailFragment newInstance(String imageUrl) {
DetailFragment fragment = new DetailFragment();
Bundle bundle = new Bundle();
bundle.putString(IMAGE_URL, imageUrl);
fragment.setArguments(bundle);
return fragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_detail, container, false);
ButterKnife.bind(this, view);
Bundle bundle = getArguments();
String imageUrl = bundle.getString(IMAGE_URL);
Glide.with(this).load(imageUrl).into(mPvShowPhoto);
mPvShowPhoto.setOnPhotoTapListener(new PhotoViewAttacher.OnPhotoTapListener() {
@Override
public void onPhotoTap(View view, float v, float v1) {
getActivity().finish();
}
@Override
public void onOutsidePhotoTap() {
}
});
return view;
}
}複製程式碼
Day five
一、段子資料的獲取
段子資料的獲取其實跟妹子模組的方法基本一樣
先編寫實體類
public class DuanziBean {
@SerializedName("group")
private GroupBean groupBean;
private String type;
public GroupBean getGroupBean() {
return groupBean;
}
public void setGroupBean(GroupBean groupBean) {
this.groupBean = groupBean;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}複製程式碼
public class GroupBean {
private String text;
private long id;
private UserBean user;
public String getText() {
return text;
}
public long getId() {
return id;
}
public UserBean getUser() {
return user;
}
public static class UserBean {
private long user_id;
private String name;
private String avatar_url;
public String getName() {
return name;
}
public String getAvatar_url() {
return avatar_url;
}
}
}複製程式碼
寫好實體類之後,使用我們之前已經封裝好的網路請求工具以及解析工具,便能將返回的資料,解析成一個包含段子實體類的 List。
二、段子的顯示
老規矩,先寫個 RecyclerView 的 Item
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:paddingLeft="8dp"
>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/duanzi_civ_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/avatar"
android:layout_gravity="center"
/>
<TextView
android:id="@+id/duanzi_tv_author"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:layout_width="match_parent"
android:layout_height="16dp"
android:text="DeveloperHaoz"
android:layout_gravity="center_vertical"
/>
</LinearLayout>
<TextView
android:id="@+id/duanzi_tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:paddingLeft="40dp"
android:paddingRight="10dp"
android:text=""
/>
<include layout="@layout/layout_app_divide"/>
</LinearLayout>複製程式碼
然後編寫將資料和介面進行繫結的 Adapter
public class DuanziAdapter extends RecyclerView.Adapter<DuanziAdapter.DuanziViewHolder>{
private Fragment mFragment;
private List<DuanziBean> mDuanziBeanList;
public DuanziAdapter(Fragment fragment, List<DuanziBean> duanziBeanList){
this.mFragment = fragment;
this.mDuanziBeanList = duanziBeanList;
}
@Override
public DuanziViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_duanzi, null);
return new DuanziViewHolder(view);
}
@Override
public void onBindViewHolder(DuanziViewHolder holder, int position) {
try {
DuanziBean duanziBean = mDuanziBeanList.get(position);
Glide.with(mFragment).load(duanziBean.getGroupBean().getUser().getAvatar_url()).into(holder.mCivAvatar);
holder.mTvContent.setText(duanziBean.getGroupBean().getText());
holder.mTvAuthor.setText(duanziBean.getGroupBean().getUser().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public int getItemCount() {
return mDuanziBeanList.size();
}
public static class DuanziViewHolder extends RecyclerView.ViewHolder{
private CircleImageView mCivAvatar;
private TextView mTvAuthor;
private TextView mTvContent;
public DuanziViewHolder(View itemView) {
super(itemView);
mCivAvatar = (CircleImageView) itemView.findViewById(R.id.duanzi_civ_avatar);
mTvAuthor = (TextView) itemView.findViewById(R.id.duanzi_tv_author);
mTvContent = (TextView) itemView.findViewById(R.id.duanzi_tv_content);
}
}
}複製程式碼
最後段子頁面中進行資料和獲取以及介面的初始化
public class DuanziFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_duanzi, container, false);
ButterKnife.bind(this, view);
initView();
initRefresh();
return view;
}
private void initRefresh() {
mRefresh.setColorSchemeResources(R.color.colorPrimary);
mRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
initView();
mRefresh.setRefreshing(false);
}
});
}
private void initView() {
VolleyHelper.sendHttpGet(getActivity(), DuanziApi.GET_DUANZI, new VolleyResponseCallback() {
@Override
public void onSuccess(String response) {
List<DuanziBean> mDuanziBeanList = GsonHelper.getDuanziBeanList(response);
mDuanziBeanList.remove(3);
mRvShowDuanzi.setLayoutManager(new LinearLayoutManager(getActivity()));
mRvShowDuanzi.setAdapter(new DuanziAdapter(DuanziFragment.this, mDuanziBeanList));
}
@Override
public void onError(VolleyError error) {
Logger.d(error);
}
});
}
}複製程式碼
以上便是本文的全部內容,這個 APP 的全部程式碼我已經分享到 Github 上了,如果覺得對你有幫助的話,就賞個 star 吧。