Android自定义组合控件:SherlockSpinner

   2016-11-22 0
核心提示:如果你觉得从头开始自定义一个View比较麻烦,那么组合几个系统现有控件来实现需求的功能,会是你很好的一个选择。一、前言 最近在项目中,需要使用Spinner来实现下拉选择功能,UI方面倒是要求不多。但是难点在于一个界面中有多个Spinner,并且有联动关系,数

如果你觉得从头开始自定义一个View比较麻烦,那么组合几个系统现有控件来实现需求的功能,会是你很好的一个选择。

一、前言

最近在项目中,需要使用Spinner来实现下拉选择功能,UI方面倒是要求不多。但是难点在于一个界面中有多个Spinner,并且有联动关系,数据需要在点击Spinner的时候请求服务器。而且当前Spinner数据依赖于前面选择的一个或多个结果,当获取到最新数据后,才显示下拉选项。

比如说我需要先选择一个仓库,再选择项目(依赖前面选择的仓库),再选择一个批次(依赖前面选择的仓库和项目),如果我在选择完仓库时,就去判断来预加载项目和批次的数据,会使依赖逻辑变得非常复杂。

这时候就想在每次点击一个Spinner的时候,去判断依赖的选项是否已经选择,未选择就提示需先选择;如果已选择,则进行网络请求,加载数据显示到下拉选项中。

(本例使用选择语言来进行演示。)

二、使用系统Spinner

1. 首先,我想到的便是使用系统的Spinner,说干就干,XML先上:

<Spinner
    android:id="@+id/spn_languages"
    android:layout_width="match_parent"
    android:layout_height="36dp"/>

2. 然后代码设置Adapter匹配数据、设置OnItemSelectedListener绑定item选择的事件:

spnLanguages = (Spinner) findViewById(R.id.spn_languages);
ArrayAdapter<String> mAdapterSystemSpinner = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mLanguages);
spnLanguages.setAdapter(mAdapterSystemSpinner);
spnLanguages.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        showMessage("Select " + mLanguages[position]);
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
        showMessage("Select Nothing.");
    }
});

OK,现在Spinner可以使用了:

3. 接着我们来设置点击事件

spnLanguages.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        showMessage("Click Spinner");
    }
});

什么情况,运行后直接崩了?

FATAL EXCEPTION: main
Process: com.sherlockshi.widget.sherlockspinner, PID: 11757
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.sherlockshi.widget.sherlockspinner/com.sherlockshi.widget.MainActivity}: java.lang.RuntimeException: Don't call setOnClickListener for an AdapterView. You probably want setOnItemClickListener instead
   ...
Caused by: java.lang.RuntimeException: Don't call setOnClickListener for an AdapterView. You probably want setOnItemClickListener instead
   at android.widget.AdapterView.setOnClickListener(AdapterView.java:798)
   at com.sherlockshi.widget.MainActivity.initSystemSpinner(MainActivity.java:169)
   at com.sherlockshi.widget.MainActivity.onCreate(MainActivity.java:48)
   ...

简单来说就是, AdapterView 不能设置Click事件,看下Spinner源码,确实是继承自 AdapterView 。(至于为什么 AdapterView 不能设置Click事件,这个暂未深究。)

4. 那我们就设置Touch事件喽:

spnLanguages.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                showMessage("Touch Spinner");
                break;
        }
        return false;
    }
});

这下确实是可以响应点击事件了,但是响应完,下拉选择就直接显示出来了,无法满足我们的需求。

而且Spinner还有个问题,一进入界面的时候,默认就会选择第一项,而我们并不需要这样的默认值。

二、CustomSpinner = EditText + ListPopupWindow

那我们是否可以使用别的控件,来实现相同的功能呢?答案是肯定的,我们用EditText来接收点击事件,而在请求完数据之后,使用ListPopupWindow来显示下拉选项,选择EditText的原因主要有以下几点:

  • 默认在底部会有带颜色的横线,Material Design风格的EditText看起来效果很不赖
  • EditText可以方便的配置上、下、左、右四个位置的小图标,我们可以在右侧放置一个向下的三角箭头,使它看起来像一个系统的Spinner

而选择ListPopupWindow则是因为:

  • 可以方便的使用下拉列表
  • 可以自由设置锚点

1. XML布局

布局文件依旧很简单,只要一个简单的EditText,配上一个右侧的下拉图标:

<EditText
    android:id="@+id/et_languages"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="right"
    android:drawableRight="@drawable/ic_dropdown"
    android:hint="Please Select..."/>

2. 代码中创建ListPopupWindow

此部分包含以下逻辑:

  • 初始化ListPopupWindow,并关联到EditText上
  • 当点击EditText时,请求数据,请求完成后,显示ListPopupWindow
  • 选中ListPopupWindow的某一项后,将此项内容更新到EditText中,并隐藏ListPopupWindow
etLanguages = (EditText) findViewById(R.id.et_languages);
etLanguages.setKeyListener(null); // 设置EditText不可编辑,等同于在xml中设置editable="false"
lpwLanguages = new ListPopupWindow(this);
mAdapterLanguages = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mLanguages);
lpwLanguages.setAdapter(mAdapterLanguages);
lpwLanguages.setAnchorView(etLanguages); //设置ListPopupWindow的锚点,即关联PopupWindow的显示位置
lpwLanguages.setModal(true); // 是否为模态,当设置为true时,会处理返回按键的事件

lpwLanguages.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        showMessage("Select " + mLanguages[position]);

        etLanguages.setText(mLanguages[position]);
        lpwLanguages.dismiss();
    }
});

// 如果使用onClick事件,会出现第一次点击只获取焦点,第二次点击才出现下拉
etLanguages.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            // Do what you want
            getDataFromNet();
        }
        return false;
    }
});

public void getDataFromNet() {
    // 延时2秒后,修改源数据,用来模拟网络请求
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            mLanguages[1] = "C+++++++++++++";
            mAdapterLanguages.notifyDataSetChanged();

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    lpwLanguages.show();
                }
            });
        }
    }).start();
}

实现效果如下所示:

3. 小结

EditText配合ListPopupWindow组合实现Spinner的功能,使用起来倒是简单,逻辑也挺清晰的,但是如果界面上有三四个Spinner,那不是就得把类似的代码写上三四遍?

其实我们并不关心内部是用ListPopupWindow或者其它的控件来实现,也不想处理任何关于ListPopupWindow的细节。我们关心的只有Spinner的初始化、适配数据、Item选择事件(ItemClick事件),如果可以,就再加上Spinner的点击事件(Click或Touch事件)、自由控制Spinner的显示时机。

那有没有简单易用的方法,可以直接像使用系统的Spinner一样,来使用EditText和ListPopupWindow的组合呢?并且可以提供Spinner的点击事件?答案是肯定的,详情且看下一节。

三、自定义组合控件

在第二部分我们可以看出,我们的控件要满足以下两大功能:

  1. 像系统Spinner一样简单易用:初始化、适配数据、Item选择事件(ItemClick事件)
  2. 支持点击事件(Click或Touch事件),自由控制下拉框显示时机

由于控件源码稍长,就不贴出来了,有兴趣可以点击文末的Github链接,源码也比较简单,只是进行控件的组合,并提供相应的方法进行调用,下面主要介绍下使用方法。

1. 引入依赖

dependencies {
    ...
    compile 'com.sherlockshi.widget:sherlockspinner:1.0.2'
}

2. 使用方法

2.1 像使用系统Spinner一样,在XML文件中使用:

<com.sherlockshi.widget.SherlockSpinner
    android:id="@+id/sherlock_spinner"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:lineColor="#00FF00"
    android:hint="Please Select..."/>

SherlockSpinner有以下属性:

lineColor : 设置底部横线的颜色

同时支持使用代码进行配置:mSherlockSpinner.setLineColor(0x00FF00);

由于SherlockSpinner继承自 EditText , 所以你可以使用EditText的其它属性,例如 gravitytextSizetextColor

2.2 还是像使用系统Spinner一样,在代码中设置 AdapterItemClickListener :

mSherlockSpinner = (SherlockSpinner) findViewById(R.id.sherlock_spinner);
ArrayAdapter<String> mAdapterLanguages = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mLanguages);
mSherlockSpinner.setAdapter(mAdapterLanguages);
mSherlockSpinner.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        showMessage("Select " + mLanguages[position]);
    }
});

以上,就可以像系统Spinner一样使用SherlockSpinner了,如果没有别的需求,这样也就够用了。如果你有点击请求网络数据,再异步显示下拉框的需求,可以看第3步的使用方法。

2.3 (可选) 如果你想在异步加载数据后,再显示出更新后的数据,你可以使用它的点击事件来处理

记住: 在获取数据后,你必须手动调用 sherlockSpinner.show() 方法来显示SherlockSpinner的下拉选项

mSherlockSpinner.setOnClickListener(new SherlockSpinner.OnClickListener() {
    @Override
    public void onClick(View v) {
        getDataFromNet();
    }
});

public void getDataFromNet() {
    // after delay 2s, modify the source data, to simulate net request
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            mLanguages[4] = "Javaaaaaaaaaaa";
            mAdapterLanguages.notifyDataSetChanged();

            // then you must manually call sherlockSpinner.show()
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mSherlockSpinner.show();
                }
            });
        }
    }).start();
}

3. 更多属性

  • 由于SherlockSpinner继承自EditText,所以你可以使用EditText的其它属性,例如 gravitytextSizetextColor
  • SherlockSpinner还有一个属性,可以设置下拉框的显示位置,即锚点设置:
mSherlockSpinner.setAnchorView(findViewById(R.id.llyt_anchor));

效果如下图中4和5的区别,在第4部分中,下拉框停靠在Spinner上;而第5部分中,下拉框停靠在Spinner所在的整行布局上,宽度更大。

四、其它

另外,一个小小的Tip:

在styles.xml文件中配置以下代码,可使下拉框带上漂亮的分割线:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ...
    <item name="android:dropDownListViewStyle">@style/mySpinnerStyle</item>
</style>

<style name="mySpinnerStyle" parent="android:Widget.ListView.DropDown">
    <item name="android:divider">#E0E0E0</item>
    <item name="android:dividerHeight">0.1dp</item>
</style>

项目代码已共享到Github: SherlockSpinner

欢迎fork、star、issue。

PS:欢迎关注SherlockShi博客

 
标签: 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 安卓中通知功能的具体实现
    安卓中通知功能的具体实现
    通知[Notification]是Android中比较有特色的功能,当某个应用程序希望给用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知实现。使用通知的步骤1、需要一个NotificationManager来获得NotificationManager manager = (NotificationManager
    02-05 安卓开发
  • Android view系统分析-setContentView
    Android view系统分析-setContentView
    第一天上班,列了一下今年要学习的东西。主要就是深入学习Android相关的系统源代码,夯实基础。对于学习Android系统源代码,也没什么大概,就从我们平常使用最基础的东西学起,也就是从view这个切入点开始学习Android的源码,在没分析源码之前,我们有的时候
    02-05 安卓开发
  • 如何进行网络视频截图/获取视频的缩略图
    如何进行网络视频截图/获取视频的缩略图
    小编导读:获取视频的缩略图,截图正在播放的视频某一帧,是在音视频开发中,常遇到的问题。本文是主要用于点播中截图视频,同时还可以获取点播视频的缩略图进行显示,留下一个问题,如下图所示, 如果要获取直播中节目视频缩略图,该怎么做呢?(ps:直播是直
  • Android NDK 层发起 HTTP 请求的问题及解决
    Android NDK 层发起 HTTP 请求的问题及解决
    前言新的一年,大家新年快乐~~鸡年大吉!本次给大家带来何老师的最新文章~虽然何老师还在过节,但依然放心不下广大开发者,在此佳节还未结束之际,给大家带来最新的技术分享~ 事件的起因不说了,总之是需要实现一个 NDK 层的网络请求。为了多端适用,还是选择
  • Android插件化(六): OpenAtlasの改写aapt以防止资源ID冲突
    Android插件化(六): OpenAtlasの改写aapt以防
    引言Android应用程序的编译中,负责资源打包的是aapt,如果不对打包后的资源ID进行控制,就会导致插件中的资源ID冲突。所以,我们需要改写aapt的源码,以达到通过某种方式传递资源ID的Package ID,通过aapt打包时获取到这个Package ID并且应用才插件资源的命名
    02-05 安卓开发
  • Android架构(一)MVP架构在Android中的实践
    Android架构(一)MVP架构在Android中的实践
    为什么要重视程序的架构设计 对程序进行架构设计的原因,归根结底是为了 提高生产力 。通过设计是程序模块化,做到模块内部的 高聚合 和模块之间的 低耦合 (如依赖注入就是低耦合的集中体现)。 这样做的好处是使得程序开发过程中,开发人员主需要专注于一点,
    02-05 安卓开发
  • 安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件 作者: 飞龙 这个教程中我们要分析一个锁机软件。像这种软件都比较简单,完全可以顺着入口看下去,但我这里还是用关键点来定位。首先这个软件的截图是这样,进入这个界面之后,除非退出模拟器,否则没办法回到桌面。上面那
    02-05 安卓开发
  • Android插件化(二):OpenAtlas插件安装过程分析
    Android插件化(二):OpenAtlas插件安装过程分析
    在前一篇博客 Android插件化(一):OpenAtlas架构以及实现原理概要 中,我们对应Android插件化存在的问题,实现原理,以及目前的实现方案进行了简单的叙述。从这篇开始,我们要深入到OpenAtlas的源码中进行插件安装过程的分析。 插件的安装分为3种:宿主启动时立
    02-05 安卓开发
  • [译] Android API 指南
    [译] Android API 指南
    众所周知,Android开发者有中文网站了,API 指南一眼看去最左侧的菜单都是中文,然而点进去内容还是很多是英文,并没有全部翻译,我这里整理了API 指南的目录,便于查看,如果之前还没有通读,现在可以好好看一遍。注意,如果标题带有英文,说明官方还没有翻
  • 使用FileProvider解决file:// URI引起的FileUriExposedException
    使用FileProvider解决file:// URI引起的FileUri
    问题以下是一段简单的代码,它调用系统的相机app来拍摄照片:void takePhoto(String cameraPhotoPath) {File cameraPhoto = new File(cameraPhotoPath);Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);takePhotoIntent.putExtra(Medi
    02-05 安卓开发
点击排行