编写一个信息查询小应用

来源:互联网 发布:gpu编程 编辑:程序博客网 时间:2024/06/04 22:47

信息查询小应用

  考虑一个简单的数据查询应用,数据为本地源Json文件,以字符串形式给出,要求做到一个列表以及多种功能。
  数据源为手机游戏《皇牌机娘》的卡片数据,Json字符串大致类似如下的结构

{    "ammoConsume": 7,    "armor": 27,    "atk": 50,    "equip": 3,    "equipDetail": [{        "Key": "机枪",        "Value": 3    }],    "hp": 26,    "id": 1,    "mainSkill": "密集乱射",    "name": "P-40战鹰",    "oilConsume": 9,    "resist": 54,    "scout": 54,    "shield": 62,    "spd": 63,    "stars": 1,    "subSkill": "连射",    "type": "格斗"}

  整个JSON文件将由这样的数据组成集合,要求应用能将数据展示并且实现一定的功能。


设计

一切从界面开始
  虽然说软件设计标准流程中最早的步骤是概要设计以及可行性研究,但对于简单项目来说直接从界面入手开始设计要快捷的多,而且很多时候只要开发流程遵循一定的规矩,原始的软件设计标准流程并不总是最好的选择。
  在这里考虑应用主要有两个页面的构成,首先第一个页面为欢迎页面,仅显示欢迎图片和版本号,在进入欢迎页面后延迟两秒跳转到主要界面。
  主要界面由一个标题栏,一个功能菜单栏和一个列表组成,考虑到数据较多,采用可展开的列表来增加交互性。
  据此可以画出设计框图如下
  初始设计框图
  列表使用可展开项目列表,即ExpandableListView,如果想要更好的动画效果可以使用第三方的AnimatedExpandableListView,也可以自定义列表。
  需要注意的是,考虑到以后可能需要拓展更多的功能页面,因此可以在主界面上采用ViewPager的形式来展示列表。
  然后可以给出列表项和展开子项的框图设计
  这里写图片描述
  列表项展示基本信息,展开后的子项展示详细数据,包括可能需要的功能按钮,至此初步设计完成。


更进一步的设计
  进一步设计包括对界面的细节作出初步的规范,对应用的功能模块进行区分,确定所需要的框架或者第三方库等等。
  在这个时候,应用功能的确定和细分是相当重要的一部分,示例应用是个数据查询应用,因此它至少需要如下几种功能。

  1. 数据展示
  2. 数据筛选
  3. 数据查找
  4. 数据对比

  当然作为一个简单的示例项目,这些功能可以不那么完善和精准,但至少应该要有;数据的展示是基本功能,只要将合适的数据放到列表中显示出来即可,因此这个功能是自然完成的;剩下的三个功能都可以被设计到菜单栏上,那么对菜单栏的详细设计框图可以画出如下。
  这里写图片描述
  其中筛选功能触摸后弹出选择弹窗,用于给出筛选选项。整个搜索栏由一个输入框和一个按钮组成,暂时设计为输入名称对数据进行搜索。
  对比功能的实现使用一种交互式实现,即触摸“对比”按钮后进入选择模式,从列表中选择所需的对比项目,选择完成后点击开始便可弹出对比结果予以展示。
  然后考虑列表项的详细设计,其中编号,名称和星级采用文本形式,头像采用图片,而类型则为图片与文本并列的形式,展开子项根据数据的特点设计为五行数据,如下所示。
  这里写图片描述
  这样便可以展示所有的子项数据。
  至此,较为详细的界面框图已经完成,接着开始区分功能模块。


功能模块区分
  安卓应用最重要的模块莫过于Activity了,如果有需要的话还要采用Fragment作为界面容器,在这个项目中选择了Fragment+Activity的模式,主界面使用一个ViewPager来展示不同的Fragment,当前情况只有一个Fragment可供展示。
  因此一个粗略的分块如下

  • StartupPage
    • WelcomeBackground
    • VersionNumber
  • MainPage
    • TitleBar
    • PageIndicator
    • ViewPager
      • AVGListFragment
        • FunctionMenu
          • FilterBtn
          • SearchBar
            • InputBox
            • SearchBtn
          • CompareBtns
            • CompareStart/Confirm
            • CompareCancel
        • AnimatedExpandableListView
  • ListGroupItem
    • Number
    • HeadImg
    • Name
    • Type
    • Stars
  • ListChildItem
    • OtherData

  按照这样的一个简单的树形结构便可以较为清晰地区分好各个不同的功能区域。
  因为示例项目非常简单,因此无需使用第三方框架或者复杂的功能库,为了方便地解析Json数据引入fastjson库即可。
  接下来便可以进行代码的编写。


代码编写

  在实际进入功能代码的编写之前,最好对代码做一个整体的抽象设计,这种设计应当具有一定的通用性,在编写不同的项目时可以拿来即用。
  这种设计的方式有很多可能性,每个不同的编程人员也会有自己的习惯,因此在这里不花费太多篇幅予以讲解,而是直接使用一个作者自己写好的抽象设计。
  非常简单易用,功能不一定很强但对于这个示例项目而言足够了。
  设计的基本概念是将所用到的组件分成三种

  1. Activity
  2. Fragment
  3. ComponentView

  然后分别为三种组件设计抽象基类,统一后续编写的逻辑结构
  其中第三种ComponentView是专门设计的子视图抽象组件,它不继承任何源自Widget的类,仅仅作为一个抽象意义上的View的集合而存在。

Activity抽象基类BaseUI

public abstract class BaseUI extends FragmentActivity {    protected Context resContext; // 上下文引用    protected ResourceManager resManager; // 资源管理器    protected View contentView; // 主要视图    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        resContext = getApplicationContext();        resManager = ResourceManager.getManager(resContext);        contentView = loadViews();        setContentView(contentView);        loadMembers();        postSetup();    }    protected abstract View loadViews(); // 加载视图组件    protected abstract void loadMembers(); // 加载成员数据    protected abstract void postSetup(); // 后处理    protected abstract void updateViews(); // 刷新界面}

Fragment抽象基类BaseFragment

public abstract class BaseFragment extends Fragment {    protected Context resContext; // 上下文引用    protected ResourceManager resManager; // 资源管理器    protected View fragmentView; // 主要视图    @Override    public void onAttach(Context context) {        super.onAttach(context);        resContext = context;        resManager = ResourceManager.getManager(resContext);    }    @Nullable    @Override    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {        fragmentView = loadViews(inflater);;        loadMembers();        postSetup();        return fragmentView;    }    protected abstract View loadViews(LayoutInflater inflater); // 加载视图组件    protected abstract void loadMembers(); // 加载成员数据    protected abstract void postSetup(); // 后处理    protected abstract void updateViews(); // 刷新界面}

ComponentView抽象基类ViewCollection

public abstract class ViewCollection {    protected Context resContext; // 上下文引用    protected ResourceManager resManager; // 资源管理器    protected View contentView; // 主要视图    public ViewCollection(Context context) {        resContext = context;        resManager = ResourceManager.getManager(resContext);    }    public View initView(@Nullable View parentView, int viewId) {        if (resContext == null) {            return null;        }        if (parentView == null) {            contentView = LayoutInflater.from(resContext).inflate(viewId, null);        } else {            contentView = parentView.findViewById(viewId);        }        loadViews();        return contentView;    }    protected abstract void loadViews(); // 加载视图组件    public abstract void updateViews(); // 刷新界面    public View getContentView() { // 获取主要视图        return contentView;    }}

  代码中用到的ResourceManager是一个辅助单例类,提供一些和资源有关的功能

ResourceManager资源管理器

public class ResourceManager {    private WeakReference<Context> refContext; // 上下文弱引用    private static ResourceManager manager = null; // 单例模式静态对象    private HashMap<String, Integer> avgTypeMap; // 用于辅助程序获得机娘类型图标的哈希表    private ResourceManager(Context context) {        refContext = new WeakReference<>(context);        avgTypeMap = new HashMap<>();        avgTypeMap.put("强攻", R.drawable.icon_avg_type_1);        avgTypeMap.put("重击", R.drawable.icon_avg_type_2);        avgTypeMap.put("支援", R.drawable.icon_avg_type_3);        avgTypeMap.put("突袭", R.drawable.icon_avg_type_4);        avgTypeMap.put("格斗", R.drawable.icon_avg_type_5);        avgTypeMap.put("爆轰", R.drawable.icon_avg_type_6);        avgTypeMap.put("狙击", R.drawable.icon_avg_type_7);    }    private static synchronized void syncInit(Context context) { // 静态同步对象创建方法        if (manager == null) {            manager = new ResourceManager(context);        }    }    public static ResourceManager getManager(Context context) { // 获得管理器单例对象        if (manager == null) {            syncInit(context.getApplicationContext());        }        return manager;    }    public int getResourceID(String resType, String resName) { // 通过名称和类型获取资源ID        return refContext.get().getResources()                .getIdentifier(resName, resType, refContext.get().getPackageName());    }    public int getAVGType(String type) { // 根据类型获取机娘图标资源ID        Integer id = avgTypeMap.get(type);        if (id == null) {            id = 0;        }        return id;    }}

  至此,代码的抽象设计完成
实际编码
  编码的方式方法万万千千,每个人都有自己的风格和方法,没有必要在这里长篇累牍地介绍详细的编码,具体的项目代码会放置在码云的公共库里,大家可随意下载。
  在这里只介绍一些重点

  • 丢掉findViewById
      每个编写安卓应用的程序员大概都曾被复杂界面下那十几乃至几十行的findViewById搞得头大,而偏偏这些语句是不可或缺的,即便有类似FindViewByMe这样的编辑器插件来协助程序员们,这些让人心生厌烦的代码还是摆在那无法消除。
      这个时候我们就可以请出“依赖注入”系列的工具了,其中以Butter-Knife为代表的View注入框架是非常有用的注入工具,它能将繁琐的findViewById操作缩减为简单的注解和成员声明,隐藏掉其获取控件的真实操作。

ButterKnife框架示例

public class StartupPage extends Activity {    private ResourceManager resManager;    // 组件对象注解    @BindView(R.id.ivWelcome)    ImageView ivWelcome;    @BindView(R.id.tvVersionWelcome)    TextView tvVersionWelcome;    private Handler forwardHandler;    private Timer forwardTimer;    private TimerTask forwardTask;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        resManager = ResourceManager.getManager(this);        setContentView(resManager.getResourceID("layout", "layout_welcome_page"));        ButterKnife.bind(this); // 该方法会自动根据注解找到对应的组件,将成员对象赋值        Random rand = new Random(System.currentTimeMillis());        int index = rand.nextInt(2);        index++;        try {            PackageManager pm = getPackageManager();            PackageInfo pi = pm.getPackageInfo(getPackageName(), 0);            String version = pi.versionName;            tvVersionWelcome.setText(version);        } catch (Exception e) {            if(tvVersionWelcome != null) {                tvVersionWelcome.setText("");            }            e.printStackTrace();        }        ivWelcome.setImageResource(resManager                .getResourceID("mipmap", "pic_welcome_page_" + index));        forwardHandler = new ForwardHandler(this);        forwardTimer = new Timer();        forwardTask = new ForwardTask();        forwardTimer.schedule(forwardTask, 3000);    }    private static class ForwardHandler extends Handler {        private WeakReference<StartupPage> actRef;        public ForwardHandler(StartupPage activity) {            actRef = new WeakReference<>(activity);        }        @Override        public void handleMessage(Message msg) {            StartupPage activity = actRef.get();            // forward to main page            Intent intent = new Intent(activity, MainPage.class);            activity.startActivity(intent);            activity.finish();        }    }    private class ForwardTask extends TimerTask {        @Override        public void run() {            if (forwardHandler != null) {                forwardHandler.sendEmptyMessage(0);            }        }    }}

  关于ButterKnife的使用,网络上有许多相关文章,比如说Android Butterknife 8.4.0 使用方法总结
  这个工具也并不单单只是用于View注入,它还有很多别的很好用的注解。

  • 对比功能的实现细节
      在设计中有对比功能的需求,而作者选择的实现方式是交互式实现,触摸“对比”按钮后进入选择模式,从列表中选择所需的对比项目,选择完成后点击开始便可弹出对比结果予以展示。
      有多种可能的实现方案,这里是比较简单粗暴的一种。
      首先是为ExpandableListView添加OnGroupClickListener,拦截掉默认的点击展开响应,改为根据情况判断是否展开。

自定义OnGroupClickListener

private class ListGroupClickListener        implements ExpandableListView.OnGroupClickListener {    @Override    public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {        if (exlvFragAvgList != null) {            if(adapterAVGList.getCompareSelectStatus()                == AVGListAdapter.COMPARE_STATUS_IDLE) {                if (exlvFragAvgList.isGroupExpanded(groupPosition)) {                    exlvFragAvgList                        .collapseGroupWithAnimation(groupPosition);                } else {                    exlvFragAvgList                        .expandGroupWithAnimation(groupPosition);                }            } else {                AVGItem result = adapterAVGList                    .selectOrUnselectGroupForComp(groupPosition);                if(result == null) {                    Toast.makeText(resContext,                    "无法选择多于三个对比对象...",                    Toast.LENGTH_LONG).show();                }                adapterAVGList.notifyDataSetChanged();            }        }        return true;    }}

  让ExpandableListView的Adapter保持一个状态标志,当选择了对比模式时改变标志来告诉这个回调应该进入哪个分支。如果不是对比模式,则正常收起和展开子项;如果处于对比模式,则不响应收起和展开,而是记录选择的项目。
  然后便可以在确定对比时根据选择情况打开对比窗口了

选择对比项和开始对比部分代码

case FunctionBarAvgListPage.FUNC_COMPARE:    // select compare item    if (fragment.adapterAVGList.getCompareSelectStatus()        == AVGListAdapter.COMPARE_STATUS_IDLE) {        fragment.adapterAVGList.enterSelectionMode();        fragment.functionBar.setCompareRunBtn(false);    } else {        fragment.adapterAVGList.quitSelectionMode();        fragment.functionBar.setCompareRunBtn(true);    }    fragment.adapterAVGList.notifyDataSetChanged();    break;case FunctionBarAvgListPage.FUNC_COMPARE_RUN:    // go for compare    if (fragment.adapterAVGList.getCompareSelectStatus()        == AVGListAdapter.COMPARE_STATUS_AVAILABLE) {        // open up a popup window for compare        fragment.comparePopWindow            = new ComparePanelPopupWindow(fragment.resContext);        fragment.comparePopWindow            .initWindow(fragment.functionBar.getContentView(),                fragment.resManager.getResourceID("layout",                    "layout_popwin_avg_list_func_compare"));        fragment.comparePopWindow.setFocusable(true);        fragment.comparePopWindow            .setCompareItems(fragment.adapterAVGList.getSelectItems());        fragment.comparePopWindow.showPopupWindow();        fragment.comparePopWindow.updateViews();    } else if(fragment.adapterAVGList.getCompareSelectCounter() < 2) {        Toast.makeText(fragment.resContext, "请选择两个或三个比较对象...",            Toast.LENGTH_LONG).show();    }

后续工作

  因为每张卡片的数据存在随着等级和强化数值的增加而变化的情况,因此为了完善功能又添加了一个属性计算器的功能,启动的入口在每个展开子项的最下面。
  这里写图片描述
  如图所示,点击属性计算器会打开对应卡片的计算器弹窗,输入等级和强化数值后便可进行计算
  至此该项目暂告一段落,这是个非常基础和简单的小项目,也是作者最早的一个独立完成的小项目(除了跟着公司做的项目之外),因此虽然它很简单甚至到了简陋的地步,但依然有不少值得记录的东西。