话不多说,先看效果图
学习android开发有几个月了,一直都是做做单个功能的小练习,最近正好有个公司用混合模式开发AppCan做的一个谈话管理项目,我用android原生将其基本实现,除了用于练习客户端与服务器的通信方法,也是对Google大力推行的Material Design的一个实践与尝试。
开发平台IDE
本项目首先在Eclipse上进行开发,但是在引用GitHub上面各个大神的Material Design Library时,非常不方便,大部分都没有封装成jar包,最后果断放弃,转用Android Studio进行开发,虽然编译速度很慢,但是在添加支持库、依赖方面实在是太给力了,下面再详细说。
Material Design
Material Design是Google公司在2014年I/O大会上提出的一种全新的设计模式,即以扁平化为主,通过引入Z轴、阴影的变化、水波纹等来体现“Material(物质、材料)”的概念。
可以说MD是未来Android的发展方向,越来越多的成熟软件都采用了该设计模式,未来必将是一种设计趋势。
点击了解 Material Design中文版
本项目特点
- 本项目使用了作为Material Design主推的RecyclerView和CardView两种控件,用于代替ListView,更大程度上发挥了列表视图的功能与扩展能力,使用FloatActionButton实现浮动按钮。
- 使用Activity嵌套ViewPager嵌套Fragment来实现页面的作用滑动,点击Tab跳转。
- 使用AsyncHttpClient框架进行网络通信,并且通过cookie来保持登录状态,实现客户端登录持久化。
AsyncHttpClient框架 - 使用了GitHub上面大量的开源UI框架,实现了Material Design风格。
Material Design开源框架库集合
项目配置
- 添加项目依赖
dependencies {
compile 'com.android.support:recyclerview-v7:22.2.0'
//v7包中的RecyclerView
compile 'com.android.support:cardview-v7:22.2.0'
//v7包中的CardView
compile 'com.android.support:appcompat-v7:22.0.1'
//v7包中的Appcompat
compile 'com.blunderer:materialdesignlibrary:2.0.4'
//GitHub开源MD框架库
compile 'com.rengwuxian.materialedittext:library:2.1.4'
//GitHub开源EditText库
compile 'com.github.rey5137:material:1.2.1'
//GitHub开源普通Button库
compile 'com.loopj.android:android-async-http:1.4.8'
//GitHub开源网络请求框架
compile 'com.getbase:floatingactionbutton:1.10.0'
//GitHub开源FAB浮动Button
}
只需在项目根目录下的build.gradle中添加上面几行代码,即可在编译时自动下载所需依赖包,这是我放弃Eclipse转用Studio最看重的一个方面,实在是太方便了。
1、启动闪屏页面实现
主流软件打开时都会有一个大概几秒钟的启动页面,此时后台可以进行网络初始化等操作。
我的xml布局就不说了,只放了两个TextView显示两行文字,
实现思路很简单,通过开启线程打开新的页面,给线程设置等待时间就可以了。
看源码:
SplashActivity.java
public class SplashActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
//此处开启线程,等待两秒后执行,进入登录页面。
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Intent mIntent = new Intent(SplashActivity.this, LoginActivity.class);
startActivity(mIntent);
//此处使用淡入淡出的切换动画
overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
finish();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
其中淡入淡出的xml资源如下(淡出相反)
R.anim.fade_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<alpha
android:duration="1000"//持续时间1s
android:fromAlpha="0.0"//开始透明度0%
android:toAlpha="1.0"//结束透明度100% />
</set>
2、登录页面实现
- 登录页面主体使用ActionBar,有一个服务器设置Button
- EditText使用框架库,实现hint文字动态悬浮,获得焦点变色等效果。
- CheckBox使用框架库,实现动画改变选中状态。
- Button使用框架库,实现扁平化设计风格和点击ripple波纹效果。
activity_login.xml (只保留主要部分)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- 此处必须使用app命名空间,才可以改变自定义控件属性 -->
<!-- 引用库中自定义EditText控件 -->
<com.rengwuxian.materialedittext.MaterialEditText
android:hint="Input your username"
app:met_baseColor="@color/color_edit_text_unfocus"
app:met_clearButton="true"
app:met_floatingLabel="highlight"
app:met_floatingLabelText="USERNAME"
app:met_iconLeft="@drawable/ic_login_username"
app:met_maxCharacters="10"
app:met_primaryColor="@color/color_primary"
app:met_singleLineEllipsis="true" />
<!-- 密码输入EditText,同上省略 -->
<com.rengwuxian.materialedittext.MaterialEditText
android:inputType="textPassword"/>
<!-- 记住密码CheckBox控件 -->
<com.rey.material.widget.CheckBox
android:id="@+id/cb_login_rempass"
style="@style/Material.Drawable.CheckBox"
android:text="Rember Password"
android:textColor="@color/color_primary"
app:cbd_strokeColor="@color/color_primary" />
<!-- 登录Buttion -->
<com.rey.material.widget.Button
style="@style/Material.Drawable.Ripple.Touch.MatchView.Light"
android:text="LOGIN"
android:textColor="@color/carbon_white"
app:rd_enable="true" />
</RelativeLayout>
登录页面Activity需要继承自com.blunderer.materialdesignlibrary.activities.Activity,然后重写需要重写的方法。
- 不要在onCreate中调用setContentView方法来设置布局资源
- 在重写方法getContentView中调用布局资源
- 需要单独封装AsyncHttpClient类,才能实现之后每次请求都保持登录状态,即设置cookie
- 使用SharePreferences来本地存储用户名和密码
- 创建用户信息类UserBean,通过get和set方法读写用户信息
- 创建服务器配置类ServerConfig来全局化配置服务器参数
- 通过重写onCreateOptionsMenu方法加载menu资源
- 通过重写onOptionsItemSelected设置menu监听
LoginActivity.java 实现源码
public class LoginActivity extends com.blunderer.materialdesignlibrary.activities.Activity{
private Button btn_login;
//其他控件省略
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//此处不写setContentView(R.layout.activity_login)
//布局加载在重写的getContentView方法中
init();//初始化数据
isSavedPassword();//自动填充密码
setOnclickListener();//按钮监听
}
/* 初始化 */
protected void init() {
//绑定控件,其他省略
et_username =(MaterialEditText)
findViewById(R.id.et_login_username);
}
/* 自动填充密码 */
protected void isSavedPassword() {
//使用SharePreferences来存储用户名和密码
SharedPreferences sharedPreferences = getSharedPreferences("UserLogin", MODE_PRIVATE);
String username = sharedPreferences.getString("username", "");
String password = sharedPreferences.getString("password", "");
if (!(username.equals("")) && !(password.equals(""))) {
cb_rempass.setChecked(true);
}
et_password.setText(password);
et_username.setText(username);
}
/* 登陆按钮监听 */
protected void setOnclickListener() {
btn_login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//从服务器配置类中取出配置参数
ServerConfig server = new ServerConfig();
String flag = server.getFLAG();
String url = server.getURL_USER_LOGIN();
String username = et_username.getText().toString();
String password = et_password.getText().toString();
//判断输入是否为空
if (!isEmptyInput(username, password)) {
doSavePassword(username, password);
//执行登录
doLogin(url, flag, username, password);
}
}
});
}
/* 空输入判断 */
protected boolean isEmptyInput(String username, String password) {
//省略不写
}
/* 记住密码 */
protected void doSavePassword(String username, String password) {
SharedPreferences sharedPreferences = getSharedPreferences("UserLogin", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
//如果CheckBox选中的话则存入帐号密码,否则存入空
if (cb_rempass.isChecked()) {
editor.putString("username", username);
editor.putString("password", password);
editor.commit();
} else {
editor.putString("username", "");
editor.putString("password", "");
editor.commit();
}
}
/* 执行登录 */
protected void doLogin(String url, String flag, String username, String password) {
// 实例化client对象,我把AsyncHttpClient另外封装为一个类,增加setCookie功能
AsyncHttpClient client = new FinalAsyncHttpClient().getAsyncHttpClient();
//不知道为什么,在登录前就要执行保存cookie,否则会失败
saveCookie(client);
// 设置请求参数
RequestParams params = new RequestParams();
params.put("flag", flag);
params.put("username", username);
params.put("password", password);
// 执行post请求
client.post(url, params, new AsyncHttpResponseHandler() {
//登录成功
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
try {
JSONObject jo = new JSONObject(new String(responseBody));
if (jo.getString("result").equals("登陆成功")) {
// 存储登录成功cookie
CookieUtils.setCookies(getCookie());
// 存储用户信息
saveUserInfoByString(new String(responseBody));
// 跳转至主页面
toMainActivity();
}
} catch (Exception e) {
e.printStackTrace();
}
}
//登录失败
@Override
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
Toast.makeText(getApplicationContext(), error.toString(), Toast.LENGTH_SHORT).show();
System.out.println("login登录失败:" + error.toString());
}
});
}
/* 用户信息存储 */
protected void saveUserInfoByString(final String userJson) {
try {
//实例化用户信息类UserBean,用于存储用户各项信息,此处省略
UserBean ub = new UserBean();
JSONObject user = new JSONObject(userJson);
ub.setUserId(user.getString("userId"));
} catch (JSONException e) {
e.printStackTrace();
}
}
/* 跳转主页 */
protected void toMainActivity() {
Intent mIntent = new Intent(LoginActivity.this, MainActivity.class);
this.startActivity(mIntent);
this.finish();
}
/* 保存cookie */
protected void saveCookie(AsyncHttpClient client) {
PersistentCookieStore cookieStore = new PersistentCookieStore(this);
client.setCookieStore(cookieStore);
}
/* 得到cookie */
protected List<Cookie> getCookie() {
PersistentCookieStore cookieStore = new PersistentCookieStore(this);
List<Cookie> cookies = cookieStore.getCookies();
return cookies;
}
//此处返回布局资源id
@Override
protected int getContentView() {
return R.layout.activity_login;
}
//允许ActionBar阴影
@Override
protected boolean enableActionBarShadow() {
return true;
}
//ActionBarHandler
@Override
protected ActionBarHandler getActionBarHandler() {
return new ActionBarDefaultHandler(this);
}
//设置Option菜单
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//此处调用menu资源
getMenuInflater().inflate(R.menu.menu_login, menu);
return super.onCreateOptionsMenu(menu);
}
//设置menu菜单点击事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
//服务器设置按钮
case R.id.btn_login_server:
Intent intent = new Intent(LoginActivity.this, ServerSettingActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}
用户信息类示例 UserBean.java
public final class UserBean {
private static String userId;//用户信息变量
//get方法
public String getUserId() {
return userId;
}
//set方法
public void setUserId(String userId) {
UserBean.userId = userId;
}
}
AsyncHttpClient封装类
public class FinalAsyncHttpClient {
private AsyncHttpClient client;
/* 构造方法 */
public FinalAsyncHttpClient() {
client = new AsyncHttpClient();//实例化对象
client.setTimeout(5);//设置超时
// 获取cookie列表
if (CookieUtils.getCookies() != null) {
BasicCookieStore bcs = new BasicCookieStore();
bcs.addCookies(CookieUtils.getCookies().toArray(
new Cookie[CookieUtils.getCookies().size()]));
client.setCookieStore(bcs);
}
}
/* 得到client对象方法 */
public AsyncHttpClient getAsyncHttpClient() {
return this.client;
}
}
cookie处理类
public class CookieUtils {
private static List<Cookie> cookies;
/* 返回cookies列表 */
public static List<Cookie> getCookies() {
return cookies != null ? cookies : new ArrayList<Cookie>();
}
/* 设置cookies列表 */
public static void setCookies(List<Cookie> cookies) {
CookieUtils.cookies = cookies;
}
}
3、主页面实现
主页面继承自com.blunderer.materialdesignlibrary.activities.ViewPagerWithTabsActivity
- 重写必须重写的方法
- 在getViewPagerHandler方法中加载Fragment对象
- expandTabs方法中,true:扩展Tab占满屏幕宽,false相反
- defaultViewPagerPageSelectedPosition方法设置默认选择tab
- onCreateOptionsMenu方法加载menu资源
- onOptionsItemSelected方法设置menu点击事件
在点击注销按钮时创建一个dialog,此处需要注意
AlertDialog.Builder builder = new AlertDialog.Builder(this);
此时参数Context可以传入this,因为是在Activity类中;
若在Fragment中,则不能使用this或是getApplicationContext( ),要使用getActivity( ),否则报错
MainActivity.java
public class MainActivity extends ViewPagerWithTabsActivity {
@Override
protected boolean enableActionBarShadow() {
return true;//允许阴影
}
@Override
public ViewPagerHandler getViewPagerHandler() {
//通过ViewPagerHandler添加Fragment页面
ViewPagerHandler handler = new ViewPagerHandler(this);
handler.addPage("页面标题一", TalkCheckFragment.newInstance());
handler.addPage("页面标题二", UserInfoFragment.newInstance());
handler.addPage("页面标题三", TalkLookFragment.newInstance());
return handler;
}
@Override
public boolean expandTabs() {
return true;//是否让Tab平均占满屏幕宽度
}
@Override
public int defaultViewPagerPageSelectedPosition() {
return 1;//进入页面时默认打开的Tab,从0开始
}
@Override
protected ActionBarHandler getActionBarHandler() {
return new ActionBarDefaultHandler(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//加载menu资源
getMenuInflater().inflate(R.menu.menu_main, menu);
return super.onCreateOptionsMenu(menu);
}
//menu中注销按钮点击事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.btn_main_menu:
//弹出注销提示框
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("提示");
builder.setMessage("确认要注销当前用户吗?");
builder.setPositiveButton("Confirm", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
finish();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.create().show();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}
4、谈话列表页面Fragment实现
谈话列表页面使用RecyclerView+CardView组合方式,实现灵活多变的卡片式列表布局,并且根据数据内容而显示不同的效果,以及卡片点击事件监听。
首先看看布局资源
fragment_talk_list.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/frame"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_main_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />
</FrameLayout>
<!-- 悬浮按钮控件 -->
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_list_newTalk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/frame"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="16dp"
fab:fab_colorNormal="@color/color_primary"
fab:fab_colorPressed="@color/color_primary_dark"
fab:fab_icon="@drawable/ic_fab_star" />
</RelativeLayout>
下面看Fragment实现源码
- 继承自android.support.v4.app.Fragment
- onCreateView方法用于加载布局资源
- newInstance方法用于获取Fragment实例
- 调用jsonArrayToMapList方法将服务端返回JsonArray转存为List< Map >对象
- bindToListView方法把数据绑定到CardView中显示
public class TalkCheckFragment extends Fragment {
private RecyclerView mRecyclerView;
private TalkListAdapter mTalkListAdapter;
private FloatingActionButton fab_createTalk;
/* 得到Fragment对象实例 */
public static TalkCheckFragment newInstance() {
TalkCheckFragment fragment = new TalkCheckFragment();
return fragment;
}
/* 获取谈话列表 */
private void getTalkList(String url, String userId) {
AsyncHttpClient client = new FinalAsyncHttpClient().getAsyncHttpClient();
RequestParams params = new RequestParams();
params.put("userid", userId);
params.put("flag", "2");
client.get(url, params, new AsyncHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
//获取服务器资源成功后转换JsonArray数据
List<HashMap<String, String>> data = jsonArrayToMapList(new String(responseBody));
bindToListView(data);
}
/* 将JsonArray转存为List<Map> */
private List<HashMap<String, String>> jsonArrayToMapList(String result) {
List<HashMap<String, String>> data = new ArrayList<>();
try {
JSONArray ja = new JSONArray(result);
for (int i = 0; i < ja.length(); i++) {
HashMap<String, String> talk = new HashMap<>();
JSONObject jo = ja.getJSONObject(i);
Iterator<String> it = jo.keys();
while (it.hasNext()) {
String key = String.valueOf(it.next());
String value = (String) jo.get(key);
talk.put(key, value);
}
data.add(talk);
}
}
return data;
}
/* 绑定数据集到列表视图 */
private void bindToListView(List<HashMap<String, String>> data) {
//给RecyclerView设置线性布局管理器
mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
//设置默认item属性动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
mRecyclerView.setHasFixedSize(true);
//实例化自定义适配器
mTalkListAdapter = new TalkListAdapter(getActivity(), data);
//设置卡片视图点击事件监听器
//OnCardViewClickListener为抽象接口,需要实现onItemClick方法
mTalkListAdapter.setOnCardViewClickListener(new TalkListAdapter.OnCardViewClickListener() {
@Override
public void onItemClick(View view, Map<String, String> data) {
String talkId = data.get("id");
System.out.println("talkid="+talkId);
}
});
//绑定适配器
mRecyclerView.setAdapter(mTalkListAdapter);
}
/* FAB悬浮按钮监听 */
private void setFabListener() {
fab_createTalk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//弹出新建谈话提示框
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("提示");
builder.setMessage("确定要新建谈话吗?");
builder.setPositiveButton("我要新建谈话", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
Intent intent = new Intent(getActivity(), NewTalkActivity.class);
startActivity(intent);
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.create().show();
}
});
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//得到谈话列表Fragment视图对象
View view = inflater.inflate(R.layout.fragment_talk_list, container, false);
//根据View对象绑定RecyclerView控件
mRecyclerView = (RecyclerView) view.findViewById(R.id.rv_main_list);
//设置FAB悬浮按钮监听
fab_createTalk = (FloatingActionButton) view.findViewById(R.id.fab_list_newTalk);
setFabListener();
//请求服务器获取谈话列表
String url = new ServerConfig().getURL_TALK_LIST();
String userId = new UserBean().getUserId();
getTalkList(url, userId);
//返回视图
return view;
}
}
5、RecyclerView自定义Adapter实现
- RecyclerView并没有ListView的onItemClickListener方法, 需要我们自己写
- 需要自己写ViewHolder类并且继承自RecyclerView.ViewHolder
- 需要自己写卡片点击事件监听器接口OnCardViewClickListener
- onCreateViewHolder方法中加载CardView布局资源
- 该Adapter需要继承RecyclerView.Adapter< TalkListAdapter.ViewHolder >,泛型需要指定为自定义的ViewHolder类
- 该Adapter需要实现View.OnClickListener接口,在onClick方法中调用自定义的卡片点击监听器接口的onItemClick方法。
public class TalkListAdapter extends RecyclerView.Adapter<TalkListAdapter.ViewHolder>
implements View.OnClickListener {
private List<HashMap<String, String>> mDataSet;//数据集
private Context mContext;//上下文对象
private OnCardViewClickListener mCardViewListener = null;//自定义卡片点击监听器
/* 构造方法,在实例化适配器时传入上下文和数据集 */
public TalkListAdapter(Context context, List<HashMap<String, String>> dataSet) {
this.mContext = context;
this.mDataSet = dataSet;
}
/* 创建卡片视图,即单个数据列表项Item */
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_card_view, parent, false);
//实例化viewHolder对象,传入卡片视图View对象
ViewHolder viewHolder = new ViewHolder(v);
//设置卡片点击监听
v.setOnClickListener(this);
return viewHolder;
}
/* 绑定ViewHolder,将数据显示到对应控件上 */
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
//根据position取出数据集中数据,并绑定至ViewHolder中对应控件
HashMap<String, String> item = mDataSet.get(position);
//取出数据并显示在控件中
holder.tv_title.setText(item.get("talktitle"));
//设置Tag标签用于点击事件得到数据
holder.itemView.setTag(item);
}
/* 得到数据集长度 */
@Override
public int getItemCount() {
return mDataSet == null ? 0 : mDataSet.size();
}
/* 实现OnClick接口方法 */
@Override
public void onClick(View v) {
//当卡片视图监听器不为null时,则执行监听器接口方法
if (mCardViewListener != null) {
//通过v.getTag得到数据,强转为Map类型
mCardViewListener.onItemClick(v, (Map<String, String>) v.getTag());
}
}
/* 绑定卡片视图点击事件监听器 */
public void setOnCardViewClickListener(OnCardViewClickListener listener) {
this.mCardViewListener = listener;
}
/* 必须实现自定义ViewHolder,继承自RecyclerView.ViewHolder */
public static class ViewHolder extends RecyclerView.ViewHolder {
//成员变量
public TextView tv_title, tv_state, tv_person, tv_org, tv_date;
public View iv_header;
//构造方法,传入View对象,绑定控件id
public ViewHolder(View v) {
super(v);
tv_title = (TextView) v.findViewById(R.id.tv_list_title);
tv_state = (TextView) v.findViewById(R.id.tv_list_state);
tv_date = (TextView) v.findViewById(R.id.tv_list_date);
tv_org = (TextView) v.findViewById(R.id.tv_list_org);
tv_person = (TextView) v.findViewById(R.id.tv_list_person);
iv_header = v.findViewById(R.id.img_card_header);
}
}
/* 卡片点击事件监听接口 */
public interface OnCardViewClickListener {
void onItemClick(View view, Map<String, String> data);
}
}
至此,该项目的主要功能基本就实现了,一些比较简单的就没写出,主要是给自己的项目练习做个总结,方便以后有需要的时候回顾,也希望能让更多初学者能从中多少获得一些收获,共同进步。
再次表达一下对Material Design的喜爱之情,真的是太棒了,看看那些大神们封装的各种控件库,真的是美的不行不行的,完全秒杀iPhone的界面设计了我觉得。
—————– 分割线 2018.05.23 ————————
自打大学毕业后,就不再进行安卓方面的研发工作了,目前从事java web开发,年代久远,源代码也找不到了,各位就不要私信我要源码了,对不住了。