知识点总结
架构构成
工程主体共分为四块:
- 列表适配器 (ListAdapter)
- 解析 JSON 数据的实体类
- 项目中所有的活动 (Activity) 和碎片 (Fragment)
- 工具类,包含网络工具 (NetUtil) 和用户信息配置工具 (ConfigUtil)
架构解析
工程有非常鲜明的流程进行,首先打开一个活动,初始化 Toolbar 和刷新 Fragment,在 Fragment 中实现具体细节,将 Activity 和 Fragment 解耦,提高活动重复使用率。
- MyBaseActivity.java
- 所有活动类的基类,大多数活动都继承自这个类
- 抽象类。类中定义了三个抽象方法,在子类中具体实现,用于初始化视图和初始化数据
- 类中还包括两个具体方法,用于初始化 Toolbar 和实现碎片的转换
- MyBaseFragment.java
- 所有碎片类的基类,大多数碎片都继承自这个类
- 抽象类。类中定义了三个抽象方法,在子类中具体实现,用于初始化视图和初始化数据
- 类中包括一个具体方法,用于实现碎片的置换
- 实现了
Serializable
接口,用于进行序列化操作
- OtherActivity.java
- OtherActivity 是所有
顶部 Toolbar 只有返回键和标题栏
的通用活动 - 因为活动的页面结构大体相同,所以只需替换标题内容和内容碎片即可,省去了大量新建活动的时间,效率大大提高
- 利用
ConfigUtil.gotoFragment(Context context, String title, baseFragment fragment)
方法提供的title
字段,填充标题内容 - 通过父类的
gotoFragment((Fragment fragment)
方法,利用ConfigUtil.gotoFragment(Context context, String title, baseFragment fragment)
方法提供的fragment
字段,刷新碎片内容
- OtherActivity 是所有
- OtherActivity 类中的 gotoFragment() 方法
- 整个项目中的核心方法,大部分活动的跳转都在这里完成
- 使用了方法的重载,利用接口,向 Bundle 中传递相关字段
竞赛五套试题总结
相同点
- 架构、引导页、主页面、个人中心
不同点
- A 套:
- 地铁查询
- 全部服务
- 新闻
- 智慧党建(限选)
- B 套:
- 活动
- 违章查询
- 门诊预约
- 智慧养老(限选)
- C 套:
- 停车场
- 智慧巴士
- 生活缴费
- 智慧环保(限选)
- D 套:
- 全部服务
- 实时天气
- 门诊预约
- 智慧党建(限选)
- E 套
- 活动
- 智慧巴士
- 违章查询
- 智慧社区(限选)
组件使用详解
1. Glide(图片加载控件)
所有用到了图片填充的部分都有 Glide 的参与,是使用频率最多的控件。
功能介绍
Glide,一个被google所推荐的图片加载库,作者是bumptech。这个库被广泛运用在google的开源项目中,包括2014年的google I/O大会上发布的官方app。
Glide滑行的意思,可以看出这个库的主旨就在于让图片加载变的流畅。现在被广泛使用,当然还是有很多开发者使用Square公司的picasso,也有两个库的对比。
使用步骤
- 基本方法
with(Context context)
需要上下文,这里还可以使用 Activity、Fragment 的对象load(String url)
这里我们所使用的一个字符串形式的网络图片的 URL,后面会讲解 load() 的更多使用方式into(ImageView imageView)
你需要显示图片的目标 ImageView
- 使用示例
String url = "http://locasthost:8080/anything.jpg";
ImageView imageView = (ImageView) findViewById(R.id.imageView);
Glide.with(context).load(url).into(imageView);
2. Banner(图片轮播控件)
工程里用到 Banner 的地方不多,一个是引导活动的实现,其他都是轮播图。引导活动和轮播图使用方法相同,暂且不表。
Banner 功能介绍
现在的绝大数 app 都有 banner 界面,实现循环播放多个广告图片和手动滑动循环等功能。因为 ViewPager 并不支持循环翻页, 所以要实现循环还得需要自己去动手,我就把项目中的控件剔了出来,希望大家觉得有用。目前框架可以进行不同样式、不同动画设置, 以及完善的 api 方法能满足大部分的需求了。
Banner 使用步骤
- step1. 在布局文件中添加 Banner,可以设置自定义属性
<com.youth.banner.Banner
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="高度自己设置" />
- step2. Banner 具体方法调用
public class BannerActivity extends AppCompatActivity {
public void useBanner() {
//—————————————————————————完整使用请阅读原开发者个人主页—————————————————————————
//—————————————————————————如果想偷懒,而又只是图片轮播————————————————————————
banner.setAdapter(new BannerImageAdapter<DataBean>(DataBean.getTestData3()) {
@Override
public void onBindView(BannerImageHolder holder, DataBean data, int position, int size) {
//图片加载自己实现
Glide.with(holder.itemView).load(data.imageUrl).into(holder.imageView);
}
})
.setIndicator(new CircleIndicator(this));
}
}
3. RecyclerView(列表展示控件)
在我们项目里,只要用到了列表的地方都有 RecyclerView,应用十分广泛。
RecyclerView 功能介绍
RecyclerView 在 2014 年就已经出来了,它的出现就是为了代替 ListView、GridView。RecyclerView 比 ListView 更高级且更具灵活性.它是一个用于显示庞大数据集的容器,可通过保持有限数量的视图进行非常有效的滚动操作。 如果您有数据集合,其中的元素将因用户操作或网络事件而在运行时发生改变,请使用 RecyclerView 。
从它类名上看,RecyclerView 代表的意义是,我只管 RecyclerView,也就是说 RecyclerView 只管回收与复用 View,其他的你可以自己去设置。可以看出其高度的解耦,给予你充分的定制自由(所以你才可以轻松的通过这个控件实现 ListView, GirdView,瀑布流等效果)。
在 ListView 中 改变列表某一个 item 数据,然后刷新列表,会回到最顶部,而 RecyclerView 可以保持原来滑动的位置不变。
RecyclerView 使用步骤
- step1. 创建 RecyclerView 的 Adapter(适配器)
- 创建RecyclerView.Adapter类的子类,泛型传入RecyclerView.ViewHolder类
- 创建RecyclerView.ViewHolder类的子类
- 在RecyclerView.ViewHolder的子类中初始化item的控件
- 重写RecyclerView.Adapter类的相关方法
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
//数据源
private List<Data> list;
//上下文
private Context context;
public RecyclerViewAdapter(Context context, List<Data> list) {
this.list = list;
this.context = context;
}
/**
* 更新数据
*
* @param list
*/
public void updateData(List list) {
this.list = list;
notifyDataSetChanged();
}
/**
* 创建ViewHolder
*
* @param viewGroup
* @param i
* @return
*/
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View item = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_rv, viewGroup, false);
return new MyViewHolder(item);
}
/**
* 通过ViewHolder对item中的控件进行控制(如:显示数据等等)
*
* @param viewHolder
* @param i
*/
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
MyViewHolder holder = (MyViewHolder) viewHolder;
Data data = list.get(i);
holder.iv.setImageResource(R.drawable.ic_launcher_background);
holder.tv.setText(data.getText());
}
/**
* 返回列表长度
*
* @return
*/
@Override
public int getItemCount() {
return list == null ? 0 : list.size();
}
/**
* 创建ViewHolder类,用来缓存item中的子控件,避免不必要的findViewById
*/
class MyViewHolder extends RecyclerView.ViewHolder {
ImageView iv;
TextView tv;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
iv = itemView.findViewById(R.id.iv);
tv = itemView.findViewById(R.id.tv);
}
}
}
- step2. 将适配器设置给 RecyclerView
private void initRecyclerView() {
// 数据源
List<Data> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
String text = String.format("mockData: %1s", i);
list.add(new Data(text));
}
RecyclerView recyclerView = findViewById(R.id.rv);
// 设置适配器
adapter = new RecyclerViewAdapter(mContext, new ArrayList<>());
recyclerView.setLayoutManager(new LinearLayoutManager(mContext, RecyclerView.VERTICAL, false));
recyclerView.setAdapter(adapter);
}
4. TabLayout (标签选择器控件)
TabLayout 是 design 库提供的控件,可以方便的实现点击标签实现刷新数据功能。在我们项目里一般用于刷新新闻数据,配合 RecyclerView 使用。
- 使用示例
private TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
// 给 tabLayout 添加标签
tabLayout.addTab(tabLayout.newTab()
.setText("tagTitle")
.setTag("tagCode"));
// 给 tabLayout 添加选择监听器
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
int tag = (int) tab.getTag();
/*
* use tag do something
*/
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
5. TextInputLayout(Material Design 风格的文本框控件)
有一些比较炫酷的动画效果(比如输入的时候,内嵌标签会浮动到内容的上方),此外还有一些特别有用的功能,如错误提示、计数等等。常用作登录界面,美观好用。
TextInputLayout 功能介绍
- EditText(或者EditText子类)的一个包装类,它主要用于在用户输入文本的时候显示一个浮动标签,也支持显示错误信息和字符计数等功能
- 支持密码可见切换按钮,通过 setPasswordVisibilityToggleEnabled(boolean)API 或者 xml 中的属性,如果设置该属性为可用(enable),那么当EditText 显示设置的密码时,会显示一个切换密码可见和隐藏的按钮
TextInputLayout 使用示例
- 带字符计数的文本框
textInputLayout = (TextInputLayout) findViewById(R.id.text_input_layout);
//设置可以计数
textInputLayout.setCounterEnabled(true);
//计数的最大值
textInputLayout.setCounterMaxLength(20);
- 显示密码可见和隐藏的切换按钮
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_layout_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:textColorHint="@color/colorHint"
app:passwordToggleEnabled="true"
app:passwordToggleTint="@color/colorHint"
app:passwordToggleDrawable="@drawable/ic_eye_grey_24dp">
<android.support.design.widget.TextInputEditText
android:id="@+id/text_input_password"
android:layout_width="match_parent"
android:layout_height="48dp"
android:hint="密码"
android:textColor="@color/black"
android:inputType="textPassword"
android:singleLine="true"/>
</android.support.design.widget.TextInputLayout>
- 显示错误信息
TextInputLayout 是可以显示错误信息的,这种需求很常见没,比如登录的时候密码错误,给出相应的提示,比 Toast 提示更加友好
textInputLayoutPassword = (TextInputLayout) findViewById(R.id.text_input_layout_password);
inputEditTextPassword = (TextInputEditText) findViewById(R.id.text_input_password);
inputEditTextPassword.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
textInputLayoutPassword.setErrorEnabled(false);
}
@Override
public void afterTextChanged(Editable s) {
}
});
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String password = inputEditTextPassword.getText().toString();
if(TextUtils.isEmpty(password)||password.length()<6){
textInputLayoutPassword.setError("密码错误不能少于6个字符");
}
}
});
当密码不正确的时候,显示错误提示,当内容发生变幻的时候,记得调用
mTextInputLayoutPassword.setErrorEnabled(false);
否则错误信息会一直显示界面上。
当然有些时候我们不需要浮动标签,或者不需要浮动标签的动画,我们可以控制,将对应属性设置为 false 就行了。
6. RadioGroup(单选按钮控件)
在 个人订单
和 更改个人信息的性别
时使用到。
RadioGroup 功能介绍
RadioButton(单选按钮)在 Android 开发中应用的非常广泛,比如一些选择项的时候,会用到单选按钮。它是一种单个圆形单选框双状态的按钮,可以选择或不选择。在 RadioButton 没有被选中时,用户能够按下或点击来选中它。但是,与复选框相反,用户一旦选中就不能够取消选中。当用户选中的时候会触发一个 OnCheckedChange 事件。
实现 RadioButton 由两部分组成,也就是 RadioButton 和 RadioGroup 配合使用。RadioGroup 是单选组合框,可以容纳多个RadioButton 的容器。在没有 RadioGroup 的情况下,RadioButton 可以全部都选中;当多个 RadioButton 被 RadioGroup 包含的情况下,RadioButton 只可以选择一个。
RadioGroup 使用示例
private RadioGroup myRadioGroup = (RadioGroup) mView.findViewById(R.id.my_radio_group);
// 选中第一个按钮
((RadioButton) myRadioGroup.getChildAt(0)).setChecked(true);
// 添加选择事件监听器
myRadioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
// do something
}
});
7. Spinner(下拉列表控件)
在 违章查询
有用到。
Spinner 功能介绍
一个数据集合的菜单,是一种常见的图形组件,与其他选择组件相比,可以有效的节省屏幕空间
Spinner 使用示例
private Spinner spinnerCarType = (Spinner) mView.findViewById(R.id.spinner_carType);
private Spinner spinnerCarPlateNum = (Spinner) mView.findViewById(R.id.spinner_car_plate_num);
String[] carTypes = {"小型汽车", "大型汽车", "挂车", "教练汽车", "临时行驶车"};
String[] carPlates = {"粤", "沪", "京", "辽"};
initSpinner(carTypes, spinnerCarType);
initSpinner(carPlates, spinnerCarPlateNum);
private void initSpinner(String[] strings, Spinner spinner) {
ArrayAdapter<String> adapter = new ArrayAdapter<>(mContext,
android.R.layout.simple_spinner_item, strings);
adapter.setDropDownViewResource(R.layout.spinner_item);
spinner.setAdapter(adapter);
spinner.setSelection(0);
}
8. ExpandableListView(二级展开树结构)
ExpandableListView 功能介绍
ExpandableListView 是默认支持二级展开树形结构,一种用于垂直滚动展示两级列表的视图,和 ListView 的不同之处就是它可以展示两级列表,分组可以单独展开显示子选项。这些选项的数据是通过 ExpandableListAdapter 关联的。
ExpandableListView 使用步骤
- step1. 创建布局文件,直接使用 ExpandableListView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ExpandableListView
android:id="@+id/expand_list"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ExpandableListView>
</LinearLayout>
- step2. 创建 ExpandableListView 的适配器adapter
这里需要继承 BaseExpandableListAdapter,然后实现它的方法
方法虽然挺多,但是好理解,看名字就能知道什么意思。这段代码只是个实例,下面贴出这个文件树结构的代码
public class ExpandListAdapter extends BaseExpandableListAdapter {
private Context context;
private List<BusLineInfoBean.RowsDTO> parentList; //文件夹数据
private List<List<BusStationBean.RowsDTO>> childList; //子文件数据
private SparseArray<TextView> textViewSparseArray; //创建一个map来存储指示器,然后根据位置改变
public interface onClick {
void click(int position);
}
private onClick onClick;
public void setIndicatorOnClick(ExpandListAdapter.onClick onClick) {
this.onClick = onClick;
}
public ExpandListAdapter(Context context, List<BusLineInfoBean.RowsDTO> parentList, List<List<BusStationBean.RowsDTO>> childList) {
this.context = context;
this.parentList = parentList;
this.childList = childList;
textViewSparseArray = new SparseArray<>();
}
public void updateParentData(List list){
this.parentList = list;
notifyDataSetChanged();
}
public void updateChildData(List list){
this.childList = list;
notifyDataSetChanged();
}
@Override //获取分组的个数(也就是这里的文件夹个数)
public int getGroupCount() {
return parentList.size();
}
@Override //获取指定分组中子选项的个数
public int getChildrenCount(int groupPosition) {
return childList.get(groupPosition).size();
}
@Override //获取指定的分组数据
public BusLineInfoBean.RowsDTO getGroup(int groupPosition) {
return parentList.get(groupPosition);
}
@Override //获取指定分组中指定子选项的数据
public BusStationBean.RowsDTO getChild(int groupPosition, int childPosition) {
return childList.get(groupPosition).get(childPosition);
}
@Override //获取指定分组的ID, 这个ID必须是唯一的
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override //获取子选项的ID, 这个ID必须是唯一的
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override //分组和子选项是否持有稳定的ID, 就是说底层数据的改变会不会影响到它们。
public boolean hasStableIds() {
return true;
}
@Override //获取显示指定分组的视图
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
View view = LayoutInflater.from(context).inflate(R.layout.expandview_bus_line_parent, parent, false);
BusLineInfoBean.RowsDTO dto = getGroup(groupPosition);
TextView tvBusLineName = (TextView) view.findViewById(R.id.tv_busLine_name);
TextView tvStartAndEnd = (TextView) view.findViewById(R.id.tv_StartAndEnd);
TextView tvPrice = (TextView) view.findViewById(R.id.tv_price);
TextView tvDistance = (TextView) view.findViewById(R.id.tv_distance);
TextView tvStartTime = (TextView) view.findViewById(R.id.tv_start_time);
TextView tvEndTime = (TextView) view.findViewById(R.id.tv_end_time);
TextView textView = (TextView) view.findViewById(R.id.textView);
LinearLayout tvExpandMore = (LinearLayout) view.findViewById(R.id.tv_expandMore);
tvBusLineName.setText(dto.getName());
tvStartAndEnd.setText(dto.getFirst() + "-" + dto.getEnd());
tvPrice.setText("票价:" + ((int) dto.getPrice()));
tvDistance.setText("里程:" + dto.getMileage());
tvStartTime.setText(dto.getStartTime());
tvEndTime.setText(dto.getEndTime());
//把要随着状态改变的imageView 指示器添加到集合里面
textViewSparseArray.put(groupPosition, textView);
tvExpandMore.setOnClickListener(v -> {
onClick.click(groupPosition);
});
//改变指示器展开或者关闭的显示
setIndicator(groupPosition, isExpanded);
return view;
}
@Override //获取显示指定分组中的指定子选项的视图
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
View view = LayoutInflater.from(context).inflate(R.layout.expandview_bus_line_children, null);
BusStationBean.RowsDTO dto = getChild(groupPosition, childPosition);
TextView tvTarget = (TextView) view.findViewById(R.id.tv_target);
TextView tvBusStationName = (TextView) view.findViewById(R.id.tv_bus_station_name);
if (childPosition == 0) {
tvTarget.setText("起点:");
} else if (isLastChild) {
tvTarget.setText("终点:");
} else {
tvTarget.setVisibility(View.INVISIBLE);
}
tvBusStationName.setText(dto.getName());
return view;
}
@Override //指定位置上的子元素是否可选中
public boolean isChildSelectable(int groupPosition, int childPosition) {
return false;
}
//设置展开收起的指示器
private void setIndicator(int position, boolean isExpanded) {
//从集合中取出指示器的imageView,改变图片的显示
if (isExpanded) {
textViewSparseArray.get(position).setBackgroundResource(R.drawable.ic_baseline_expand_less_24);
} else {
textViewSparseArray.get(position).setBackgroundResource(R.drawable.ic_baseline_expand_more_24);
}
}
}
- step3. 在活动中使用 ExpandListAdapter
默认的实在是太丑了,这里为 ExpandableListView 设置适配器以及隐藏掉默认的指示器
private ExpandableListView expandableListBusLine;
BusLineListAdapter adapter = new BusLineListAdapter(mContext, new ArrayList<>(), new ArrayList<>());
//把指示器设为null
expandableListBusLine.setGroupIndicator(null);
expandableListBusLine.setAdapter(adapter);
expandableListBusLine.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
@Override
public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
ConfigUtil.gotoFragment(bundle -> {
bundle.putSerializable("lineInfo", lineInfoBeans.get(groupPosition));
}, mContext, "巴士详情", new BusLineDetailFragment());
return true;
}
});
adapter.setIndicatorOnClick(position -> {
if (expandableListBusLine.isGroupExpanded(position)) {
// 收回列表
expandableListBusLine.collapseGroup(position);
} else {
// 展开列表
expandableListBusLine.expandGroup(position);
}
});
关于展开、合并的指示器
- 可以把setIndicator方法的调用放在点击事件里面,每次点击的时候去实现展开关闭的操作。
- 如果重写点击事件方法,他是默认展开的。如果我们把
return false
变为return true
,而不给他指定操作,他就不会展开了。 - 返回
true
是表示这个点击事件“我”来做处理,处理完了,但是什么没有做,要自己写操作