知识点总结
架构构成
工程主体共分为四块:
- 列表适配器 (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.Bannerxmlns: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()) {@Overridepublic 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@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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() {@Overridepublic void onTabSelected(TabLayout.Tab tab) {int tag = (int) tab.getTag();/** use tag do something*/}@Overridepublic void onTabUnselected(TabLayout.Tab tab) {}@Overridepublic 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.TextInputLayoutandroid: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.TextInputEditTextandroid: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() {@Overridepublic void beforeTextChanged(CharSequence s, int start, int count, int after) {}@Overridepublic void onTextChanged(CharSequence s, int start, int before, int count) {textInputLayoutPassword.setErrorEnabled(false);}@Overridepublic void afterTextChanged(Editable s) {}});findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {@Overridepublic 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() {@Overridepublic 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"><ExpandableListViewandroid: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是表示这个点击事件“我”来做处理,处理完了,但是什么没有做,要自己写操作
