1. 程式人生 > >手把手教你從零開始做一個好看的 APP

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

@+ error 教你 教授 wip rac tco 需要 apt

前言

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

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

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

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

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

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

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

在開始寫正文之前,先來一波效果的展示,看看五天過後我們能實現怎樣的效果
技術分享圖片

本次的教程分為 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 吧。


猜你喜歡

  • Android 一款十分簡潔、優雅的日記 APP
  • Android 能讓你少走彎路的幹貨整理
  • Android 擼起袖子,自己封裝 DialogFragment

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