知识点总结

架构构成

工程主体共分为四块:

  1. 列表适配器 (ListAdapter)
  2. 解析 JSON 数据的实体类
  3. 项目中所有的活动 (Activity) 和碎片 (Fragment)
  4. 工具类,包含网络工具 (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 类中的 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
  • 使用示例
  1. String url = "http://locasthost:8080/anything.jpg";
  2. ImageView imageView = (ImageView) findViewById(R.id.imageView);
  3. Glide.with(context).load(url).into(imageView);

2. Banner(图片轮播控件)

工程里用到 Banner 的地方不多,一个是引导活动的实现,其他都是轮播图。引导活动和轮播图使用方法相同,暂且不表。

Banner 功能介绍

现在的绝大数 app 都有 banner 界面,实现循环播放多个广告图片和手动滑动循环等功能。因为 ViewPager 并不支持循环翻页, 所以要实现循环还得需要自己去动手,我就把项目中的控件剔了出来,希望大家觉得有用。目前框架可以进行不同样式、不同动画设置, 以及完善的 api 方法能满足大部分的需求了。

Banner 使用步骤

  • step1. 在布局文件中添加 Banner,可以设置自定义属性
  1. <com.youth.banner.Banner
  2. xmlns:app="http://schemas.android.com/apk/res-auto"
  3. android:id="@+id/banner"
  4. android:layout_width="match_parent"
  5. android:layout_height="高度自己设置" />
  • step2. Banner 具体方法调用
  1. public class BannerActivity extends AppCompatActivity {
  2. public void useBanner() {
  3. //—————————————————————————完整使用请阅读原开发者个人主页—————————————————————————
  4. //—————————————————————————如果想偷懒,而又只是图片轮播————————————————————————
  5. banner.setAdapter(new BannerImageAdapter<DataBean>(DataBean.getTestData3()) {
  6. @Override
  7. public void onBindView(BannerImageHolder holder, DataBean data, int position, int size) {
  8. //图片加载自己实现
  9. Glide.with(holder.itemView).load(data.imageUrl).into(holder.imageView);
  10. }
  11. })
  12. .setIndicator(new CircleIndicator(this));
  13. }
  14. }

3. RecyclerView(列表展示控件)

在我们项目里,只要用到了列表的地方都有 RecyclerView,应用十分广泛。

RecyclerView 功能介绍

RecyclerView 在 2014 年就已经出来了,它的出现就是为了代替 ListView、GridView。RecyclerView 比 ListView 更高级且更具灵活性.它是一个用于显示庞大数据集的容器,可通过保持有限数量的视图进行非常有效的滚动操作。 如果您有数据集合,其中的元素将因用户操作或网络事件而在运行时发生改变,请使用 RecyclerView 。

从它类名上看,RecyclerView 代表的意义是,我只管 RecyclerView,也就是说 RecyclerView 只管回收与复用 View,其他的你可以自己去设置。可以看出其高度的解耦,给予你充分的定制自由(所以你才可以轻松的通过这个控件实现 ListView, GirdView,瀑布流等效果)。

在 ListView 中 改变列表某一个 item 数据,然后刷新列表,会回到最顶部,而 RecyclerView 可以保持原来滑动的位置不变。

RecyclerView 使用步骤

  • step1. 创建 RecyclerView 的 Adapter(适配器)
    1. 创建RecyclerView.Adapter类的子类,泛型传入RecyclerView.ViewHolder类
    2. 创建RecyclerView.ViewHolder类的子类
    3. 在RecyclerView.ViewHolder的子类中初始化item的控件
    4. 重写RecyclerView.Adapter类的相关方法
  1. public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
  2. //数据源
  3. private List<Data> list;
  4. //上下文
  5. private Context context;
  6. public RecyclerViewAdapter(Context context, List<Data> list) {
  7. this.list = list;
  8. this.context = context;
  9. }
  10. /**
  11. * 更新数据
  12. *
  13. * @param list
  14. */
  15. public void updateData(List list) {
  16. this.list = list;
  17. notifyDataSetChanged();
  18. }
  19. /**
  20. * 创建ViewHolder
  21. *
  22. * @param viewGroup
  23. * @param i
  24. * @return
  25. */
  26. @NonNull
  27. @Override
  28. public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
  29. View item = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_rv, viewGroup, false);
  30. return new MyViewHolder(item);
  31. }
  32. /**
  33. * 通过ViewHolder对item中的控件进行控制(如:显示数据等等)
  34. *
  35. * @param viewHolder
  36. * @param i
  37. */
  38. @Override
  39. public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
  40. MyViewHolder holder = (MyViewHolder) viewHolder;
  41. Data data = list.get(i);
  42. holder.iv.setImageResource(R.drawable.ic_launcher_background);
  43. holder.tv.setText(data.getText());
  44. }
  45. /**
  46. * 返回列表长度
  47. *
  48. * @return
  49. */
  50. @Override
  51. public int getItemCount() {
  52. return list == null ? 0 : list.size();
  53. }
  54. /**
  55. * 创建ViewHolder类,用来缓存item中的子控件,避免不必要的findViewById
  56. */
  57. class MyViewHolder extends RecyclerView.ViewHolder {
  58. ImageView iv;
  59. TextView tv;
  60. public MyViewHolder(@NonNull View itemView) {
  61. super(itemView);
  62. iv = itemView.findViewById(R.id.iv);
  63. tv = itemView.findViewById(R.id.tv);
  64. }
  65. }
  66. }
  • step2. 将适配器设置给 RecyclerView
  1. private void initRecyclerView() {
  2. // 数据源
  3. List<Data> list = new ArrayList<>();
  4. for (int i = 0; i < 10; i++) {
  5. String text = String.format("mockData: %1s", i);
  6. list.add(new Data(text));
  7. }
  8. RecyclerView recyclerView = findViewById(R.id.rv);
  9. // 设置适配器
  10. adapter = new RecyclerViewAdapter(mContext, new ArrayList<>());
  11. recyclerView.setLayoutManager(new LinearLayoutManager(mContext, RecyclerView.VERTICAL, false));
  12. recyclerView.setAdapter(adapter);
  13. }

4. TabLayout (标签选择器控件)

TabLayout 是 design 库提供的控件,可以方便的实现点击标签实现刷新数据功能。在我们项目里一般用于刷新新闻数据,配合 RecyclerView 使用。

  • 使用示例
  1. private TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
  2. // 给 tabLayout 添加标签
  3. tabLayout.addTab(tabLayout.newTab()
  4. .setText("tagTitle")
  5. .setTag("tagCode"));
  6. // 给 tabLayout 添加选择监听器
  7. tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
  8. @Override
  9. public void onTabSelected(TabLayout.Tab tab) {
  10. int tag = (int) tab.getTag();
  11. /*
  12. * use tag do something
  13. */
  14. }
  15. @Override
  16. public void onTabUnselected(TabLayout.Tab tab) {
  17. }
  18. @Override
  19. public void onTabReselected(TabLayout.Tab tab) {
  20. }
  21. });

5. TextInputLayout(Material Design 风格的文本框控件)

有一些比较炫酷的动画效果(比如输入的时候,内嵌标签会浮动到内容的上方),此外还有一些特别有用的功能,如错误提示、计数等等。常用作登录界面,美观好用。

TextInputLayout 功能介绍

  • EditText(或者EditText子类)的一个包装类,它主要用于在用户输入文本的时候显示一个浮动标签,也支持显示错误信息和字符计数等功能
  • 支持密码可见切换按钮,通过 setPasswordVisibilityToggleEnabled(boolean)API 或者 xml 中的属性,如果设置该属性为可用(enable),那么当EditText 显示设置的密码时,会显示一个切换密码可见和隐藏的按钮

TextInputLayout 使用示例

  • 带字符计数的文本框
  1. textInputLayout = (TextInputLayout) findViewById(R.id.text_input_layout);
  2. //设置可以计数
  3. textInputLayout.setCounterEnabled(true);
  4. //计数的最大值
  5. textInputLayout.setCounterMaxLength(20);
  • 显示密码可见和隐藏的切换按钮
  1. <android.support.design.widget.TextInputLayout
  2. android:id="@+id/text_input_layout_password"
  3. android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:paddingLeft="15dp"
  6. android:paddingRight="15dp"
  7. android:textColorHint="@color/colorHint"
  8. app:passwordToggleEnabled="true"
  9. app:passwordToggleTint="@color/colorHint"
  10. app:passwordToggleDrawable="@drawable/ic_eye_grey_24dp">
  11. <android.support.design.widget.TextInputEditText
  12. android:id="@+id/text_input_password"
  13. android:layout_width="match_parent"
  14. android:layout_height="48dp"
  15. android:hint="密码"
  16. android:textColor="@color/black"
  17. android:inputType="textPassword"
  18. android:singleLine="true"/>
  19. </android.support.design.widget.TextInputLayout>
  • 显示错误信息

TextInputLayout 是可以显示错误信息的,这种需求很常见没,比如登录的时候密码错误,给出相应的提示,比 Toast 提示更加友好

  1. textInputLayoutPassword = (TextInputLayout) findViewById(R.id.text_input_layout_password);
  2. inputEditTextPassword = (TextInputEditText) findViewById(R.id.text_input_password);
  3. inputEditTextPassword.addTextChangedListener(new TextWatcher() {
  4. @Override
  5. public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  6. }
  7. @Override
  8. public void onTextChanged(CharSequence s, int start, int before, int count) {
  9. textInputLayoutPassword.setErrorEnabled(false);
  10. }
  11. @Override
  12. public void afterTextChanged(Editable s) {
  13. }
  14. });
  15. findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
  16. @Override
  17. public void onClick(View v) {
  18. String password = inputEditTextPassword.getText().toString();
  19. if(TextUtils.isEmpty(password)||password.length()<6){
  20. textInputLayoutPassword.setError("密码错误不能少于6个字符");
  21. }
  22. }
  23. });

当密码不正确的时候,显示错误提示,当内容发生变幻的时候,记得调用

  1. mTextInputLayoutPassword.setErrorEnabled(false);

否则错误信息会一直显示界面上。

当然有些时候我们不需要浮动标签,或者不需要浮动标签的动画,我们可以控制,将对应属性设置为 false 就行了。

6. RadioGroup(单选按钮控件)

个人订单更改个人信息的性别 时使用到。

RadioGroup 功能介绍

RadioButton(单选按钮)在 Android 开发中应用的非常广泛,比如一些选择项的时候,会用到单选按钮。它是一种单个圆形单选框双状态的按钮,可以选择或不选择。在 RadioButton 没有被选中时,用户能够按下或点击来选中它。但是,与复选框相反,用户一旦选中就不能够取消选中。当用户选中的时候会触发一个 OnCheckedChange 事件。

实现 RadioButton 由两部分组成,也就是 RadioButton 和 RadioGroup 配合使用。RadioGroup 是单选组合框,可以容纳多个RadioButton 的容器。在没有 RadioGroup 的情况下,RadioButton 可以全部都选中;当多个 RadioButton 被 RadioGroup 包含的情况下,RadioButton 只可以选择一个。

RadioGroup 使用示例

  1. private RadioGroup myRadioGroup = (RadioGroup) mView.findViewById(R.id.my_radio_group);
  2. // 选中第一个按钮
  3. ((RadioButton) myRadioGroup.getChildAt(0)).setChecked(true);
  4. // 添加选择事件监听器
  5. myRadioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
  6. @Override
  7. public void onCheckedChanged(RadioGroup group, int checkedId) {
  8. // do something
  9. }
  10. });

7. Spinner(下拉列表控件)

违章查询 有用到。

Spinner 功能介绍

一个数据集合的菜单,是一种常见的图形组件,与其他选择组件相比,可以有效的节省屏幕空间

Spinner 使用示例

  1. private Spinner spinnerCarType = (Spinner) mView.findViewById(R.id.spinner_carType);
  2. private Spinner spinnerCarPlateNum = (Spinner) mView.findViewById(R.id.spinner_car_plate_num);
  3. String[] carTypes = {"小型汽车", "大型汽车", "挂车", "教练汽车", "临时行驶车"};
  4. String[] carPlates = {"粤", "沪", "京", "辽"};
  5. initSpinner(carTypes, spinnerCarType);
  6. initSpinner(carPlates, spinnerCarPlateNum);
  7. private void initSpinner(String[] strings, Spinner spinner) {
  8. ArrayAdapter<String> adapter = new ArrayAdapter<>(mContext,
  9. android.R.layout.simple_spinner_item, strings);
  10. adapter.setDropDownViewResource(R.layout.spinner_item);
  11. spinner.setAdapter(adapter);
  12. spinner.setSelection(0);
  13. }

8. ExpandableListView(二级展开树结构)

ExpandableListView 功能介绍

ExpandableListView 是默认支持二级展开树形结构,一种用于垂直滚动展示两级列表的视图,和 ListView 的不同之处就是它可以展示两级列表,分组可以单独展开显示子选项。这些选项的数据是通过 ExpandableListAdapter 关联的。

ExpandableListView 使用步骤

  • step1. 创建布局文件,直接使用 ExpandableListView
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical">
  6. <ExpandableListView
  7. android:id="@+id/expand_list"
  8. android:layout_width="match_parent"
  9. android:layout_height="match_parent">
  10. </ExpandableListView>
  11. </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 是表示这个点击事件“我”来做处理,处理完了,但是什么没有做,要自己写操作