手把手教你從零開始做一個好看的 APP

本篇文章已授權為微信公眾號 code小生 發布
轉載請注明出處:http://www.lxweimin.com/p/8d2d74d6046f

前言

從零開始,手把手帶你實現一個「專注睡前的 APP」。睡覺之前如果能有一個 APP,能讓我們寫一寫這一天的見聞或者心得,同時又能看一會段子、瞄一會好看的妹子,放松一下疲憊的身心那該多好,這也是我完成這個 APP 的原因。APP 的全部代碼我已經分享到 Github 上了,需要的直接 點擊這里,如果喜歡的話,麻煩給個 star,謝謝啦。

本文為這一系列文章的總述,如果覺得篇幅過長,請點擊下面的連接

手把手教你從零開始做一個好看的 APP - Day one

手把手教你從零開始做一個好看的 APP - Day two

手把手教你從零開始做一個好看的 APP - Day three

手把手教你從零開始做一個好看的 APP - Day four

手把手教你從零開始做一個好看的 APP - Day five

在開始寫正文之前,先來一波效果的展示,看看五天過后我們能實現怎樣的效果


SleepHelper.gif

本次的教程分為 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 的配色就是用這個網站完成的,貼幾張圖片,讓你感受一下它的強大

material design palette
Material Design 風格的圖標

借助這個網站便能讓我們完成 APP 的配色以及圖標的收集,為下一步功能的實現,先打好了基礎,至于界面的設計就仁者見仁智者見智了,篇幅有限,我就不多講了。

APP 的最終設計效果如下:


SleepHelper

二、公共類的實現

因為這個項目有三個模塊,有一些東西其實是可以通用的,如果我們先把這些能夠通用的東西,封裝起來,供給所有的模塊調用的話,相信會大大提高我們的開發效率。

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 吧。


猜你喜歡

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容