在开发中很常见一些选择器,例如地址选择器、性别选择器等等,这里主要讲的是如何自定义一个选择器~
最近(2022.9)要离开当前公司了,正好有时间好好整理一下最近的收获,向大牛们再奋进奋进,10.24留下此篇留念 ~
因为是新项目,所以一切从零开始,开发时间比较紧凑,用到了很多三方框架,其中关于选择器控件 我之前也记录过一些,这次是直接从github
又找到了 AndroidPicker
待优化
- 前期了解
- 准备工作
- 核心控件
- 使用方式
关于最终自定义实现的地址选择器效果(切记这里仅是示例,完全可以自定义同等类型的View),之后有机会我会进行补全...
前期了解
虽然说是实现一款自定义的选择器控件
,但是终究站在了别人肩膀上,我用了俩款三方框架
,然后梳理内部、外部逻辑
得以使用!
我一开始用的是 AndroidPicker,但是因为无法满足需求才开始自定义选择器
的,所以我在篇内用的 WheelView 是 AndroidPicker 对应包下的内容
,但其本质还是用的 WheelView ,然后我还用到了 XPopup 优化弹窗效果
其实要更好的自定义选择器的话,最好要了解一下 WheelView ,但是关于WheelView的使用文档描述不太详细,有机会我会补充一下 ~
准备工作
build(app) - 引入依赖
implementation "com.contrarywind:Android-PickerView:4.1.9"
implementation 'com.github.li-xiaojun:XPopup:2.8.14'
AddressPickerEntity - 将选中数据传到选择器时用到的实体类,记得实例化
package com.xxx.xx.bean
import java.io.Serializable
open class AddressPickerEntity : Serializable {
/**
* 省
*/
var province: String = ""
/**
* 省 id
*/
var provinceId: String = ""
/**
* 市
*/
var city: String = ""
/**
* 市 id
*/
var cityId: String = ""
/**
* 区
*/
var county: String = ""
/**
* 区 id
*/
var countyId: String = ""
/**
* 是否显示区
*/
var disState: Boolean = false
}
AddressResponse - 省市区json对应的数据实体类(每个人可能都不同,具体看后台吧)
package com.xxx.xx.model.bean
import java.io.Serializable
data class AddressResponse(
val child: List<Child>,
val firstLetter: String,
val id: String,
val level: Int,
val name: String,
val parentId: Int,
val part: Int
) : Serializable {
data class Child(
val child: List<ChildX>,
val firstLetter: String,
val id: String,
val level: Int,
val name: String,
val parentId: Int,
val part: Int
) : Serializable {
data class ChildX(
val child: List<Any>,
val firstLetter: String,
val id: String,
val level: Int,
val name: String,
val parentId: Int,
val part: Int
) : Serializable
}
}
SimpleCallBack - 选择器选中的回调接口
package com.xxx.xx.view.popup
interface SimpleCallBack<T> {
fun call(t: T)
}
核心控件
这里就是我们的核心了:开始自定义一个属于自己的选择器
PickerAddressPopup
package com.xxx.xx.view.popup
import android.content.Context
import android.graphics.Color
import android.widget.TextView
import com.bigkoo.pickerview.adapter.ArrayWheelAdapter
import com.contrarywind.view.WheelView
import com.jsmedia.jsmanager.R
import com.jsmedia.jsmanager.bean.AddressPickerEntity
import com.jsmedia.jsmanager.model.bean.AddressResponse
import com.lxj.xpopup.XPopup
import com.lxj.xpopup.core.BasePopupView
import com.lxj.xpopup.core.BottomPopupView
import com.lxj.xpopup.enums.PopupPosition
/**
* 定制化地址选择
*/
class PickerAddressPopup : BottomPopupView {
private var mContext: Context = context
private var popupView: BasePopupView? = null
private var wl_province: WheelView? = null
private var wl_city: WheelView? = null
private var wl_county: WheelView? = null
var provinceList = ArrayList<AddressResponse>()
//省、市、区对应列表
private val provinceStrList = ArrayList<String>()
private var cityStrList = ArrayList<String>()
private var countyStrList = ArrayList<String>()
//默认数据
private var province: String = "上海市"
private var city: String = "上海市"
private var county: String = "浦东新区"
private var cityId: String = ""
private var curProvincePos = 0
private var curCityPos = 0
private var curCountyPos = 0
var simpleCallBack: SimpleCallBack<AddressPickerEntity>? = null
var intentAddress: AddressPickerEntity? = null
/**
* @param context 上下文
* @param intentAddress 用户传递所选数据的实体类,
* @param intentList 省市区数据(可以是后台返回的json格式对应数据,也可以做本地)
* @param simpleCallBack 选择后的结果回调
* */
constructor(
context: Context,
intentAddress: AddressPickerEntity,
intentList: ArrayList<AddressResponse>,
simpleCallBack: SimpleCallBack<AddressPickerEntity>
) : super(context) {
this.simpleCallBack = simpleCallBack
this.provinceList = intentList
this.intentAddress = intentAddress
}
override fun onCreate() {
super.onCreate()
wl_province = findViewById(R.id.wl_province)
wl_city = findViewById(R.id.wl_city)
wl_county = findViewById(R.id.wl_county)
//WheelView的setCyclic方法:主要看是否要允许内部数据无限滚动
wl_province?.setCyclic(false)
wl_city?.setCyclic(false)
wl_county?.setCyclic(false)
initData()
wl_province?.adapter = ArrayWheelAdapter(provinceStrList)
wl_city?.adapter = ArrayWheelAdapter(cityStrList)
wl_county?.adapter = ArrayWheelAdapter(countyStrList)
addListener()
}
private fun addListener() {
findViewById<TextView>(R.id.tv_cancel).setOnClickListener {
dismiss()
}
//用户点击确定后的逻辑,主要用于回调选中省、市、区结果
findViewById<TextView>(R.id.tv_verify).setOnClickListener {
val addressEntity = AddressPickerEntity()
addressEntity.province = province
addressEntity.city = city
addressEntity.county = county
addressEntity.cityId = cityId
/* timeEntity.weeks = weekAdapter.getWeekSelected()
timeEntity.yTime = TimePickerEntity.Time(yyHour, yyMinute)
timeEntity.xTime = TimePickerEntity.Time(xyHour, xyMinute)*/
//Kt 接口回调
simpleCallBack?.call(addressEntity)
dismiss()
}
//市:WheelView 选中后的逻辑
wl_province?.setOnItemSelectedListener {
province = provinceStrList[it]
//var cityChild = addressList[it].child as ArrayList<AddressResponse.Child>
var cityChild = provinceList[it].child as ArrayList<AddressResponse.Child>
/* cityList.clear()
cityList.addAll(cityChild)*/
city = cityChild[0].name
cityId = cityChild[0].id
if (cityChild.isNotEmpty()) {
cityStrList.clear()
for (i in cityChild.indices) {
cityStrList.add(cityChild[i].name)
}
var countyChild = cityChild[0].child
if (countyChild.isNotEmpty()) {
countyStrList.clear()
for (i in countyChild.indices) {
countyStrList.add(countyChild[i].name)
}
}
}
curCityPos = 0
curCountyPos = 0
curProvincePos = it
wl_city?.currentItem = curCityPos
wl_county?.currentItem = curCountyPos
wl_city?.adapter = ArrayWheelAdapter(cityStrList)
wl_county?.adapter = ArrayWheelAdapter(countyStrList)
}
//市:WheelView 选中后的逻辑
wl_city?.setOnItemSelectedListener {
city = cityStrList[it]
var child = provinceList[curProvincePos].child[it].child
if (child.isNotEmpty()) {
countyStrList.clear()
for (i in child.indices) {
countyStrList.add(child[i].name)
}
}
cityId = provinceList[curProvincePos].child[it].id
curCountyPos = 0
wl_county?.currentItem = curCountyPos
wl_county?.adapter = ArrayWheelAdapter(countyStrList)
}
wl_county?.setOnItemSelectedListener {
county = countyStrList[it]
}
}
/**
* 初始化数据、数据定位
*/
private fun initData() {
provinceStrList.clear()
cityStrList.clear()
countyStrList.clear()
//WheelView的setItemsVisibleCount、setDividerColor方法:主要设置内部显示数据、颜色
wl_province?.setItemsVisibleCount(10)
wl_province?.setDividerColor(Color.TRANSPARENT)
wl_city?.setItemsVisibleCount(10)
wl_city?.setDividerColor(Color.TRANSPARENT)
wl_county?.setItemsVisibleCount(10)
wl_county?.setDividerColor(Color.TRANSPARENT)
//查看外部传递的数据,之后用于定位选择器的选中效果
if (!intentAddress?.province.isNullOrEmpty()) {
province = intentAddress?.province.toString()
}
if (!intentAddress?.city.isNullOrEmpty()) {
city = intentAddress?.city.toString()
}
if (!intentAddress?.cityId.isNullOrEmpty()) {
cityId = intentAddress?.cityId.toString()
}
if (!intentAddress?.county.isNullOrEmpty()) {
county = intentAddress?.county.toString()
}
/* province = intentAddress?.province.toString()
city = intentAddress?.city.toString()
county = intentAddress?.county.toString()
cityId = intentAddress?.cityId.toString()*/
//扩展方法:因为有的需求是显示省、市,有的要显示省、市、区
if (intentAddress?.disState == true) {
wl_county?.visibility = VISIBLE
} else {
wl_county?.visibility = GONE
}
/*curProvincePos = 0
curCityPos = 0
curCountyPos = 0*/
//通过外部传递进来的 “省名称” 定位到对应 list角标值
for (i in 0 until provinceList.size) {
if (provinceList[i].name == province) {
curProvincePos = i
break
}
}
for (i in 0 until provinceList.size) {
provinceStrList.add(provinceList[i].name)
}
/* cityList = provinceList[curProvincePos].child as ArrayList<AddressResponse.Child>
for (i in cityList.indices) {
if (cityList[i].name == city) {
curCityPos = i
break
}
}
for (i in 0 until cityList.size) {
cityStrList.add(cityList[i].name)
}
*/
//通过外部传递进来的 “市名称” 定位到对应 list角标值
for (i in provinceList[curProvincePos].child.indices) {
if (provinceList[curProvincePos].child[i].name == city) {
curCityPos = i
break
}
}
for (i in 0 until provinceList[curProvincePos].child.size) {
cityStrList.add(provinceList[curProvincePos].child[i].name)
}
//通过外部传递进来的 “区名称” 定位到对应 list角标值
county.isNotEmpty().run {
for (i in provinceList[curProvincePos].child[curCityPos].child.indices) {
if (provinceList[curProvincePos].child[curCityPos].child[i].name == county) {
curCountyPos = i
break
}
}
for (i in 0 until provinceList[curProvincePos].child[curCityPos].child.size) {
countyStrList.add(provinceList[curProvincePos].child[curCityPos].child[i].name)
}
}
//及时更新WhellView内部list的角标值
wl_province?.currentItem = curProvincePos
wl_city?.currentItem = curCityPos
wl_county?.currentItem = curCountyPos
}
override fun getImplLayoutId(): Int {
return R.layout.popup_address_picker
}
override fun getMaxWidth(): Int {
return super.getMaxWidth()
}
override fun getPopupWidth(): Int {
return 0
}
fun showPopup() {
if (popupView == null) {
popupView = XPopup.Builder(mContext).enableDrag(false)
.autoDismiss(true)
.popupPosition(PopupPosition.Bottom)
.asCustom(this)
}
popupView?.show()
}
}
layout.popup_address_picker
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:background="@color/white"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_48">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_cancel"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:paddingLeft="@dimen/dp_16"
android:paddingRight="@dimen/dp_16"
android:text="取消"
android:textColor="#FF5B5B5B"
android:textSize="@dimen/sp_16" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_verify"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right"
android:gravity="center"
android:paddingLeft="@dimen/dp_16"
android:paddingRight="@dimen/dp_16"
android:text="确定"
android:textColor="@color/black_title"
android:textSize="@dimen/sp_16" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="horizontal">
<com.contrarywind.view.WheelView
android:id="@+id/wl_province"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/dp_300"
app:wheel_itemTextColor="#AAAAAA"
app:wheel_itemTextColorSelected="@color/black_title"
app:wheel_itemTextSize="@dimen/sp_16"
app:wheel_visibleItemCount="9" />
<com.contrarywind.view.WheelView
android:id="@+id/wl_city"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/dp_300"
app:wheel_itemTextColor="#AAAAAA"
app:wheel_itemTextColorSelected="@color/black_title"
app:wheel_itemTextSize="@dimen/sp_16"
app:wheel_visibleItemCount="9" />
<com.contrarywind.view.WheelView
android:id="@+id/wl_county"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/dp_300"
app:wheel_itemTextColor="#AAAAAA"
app:wheel_itemTextColorSelected="@color/black_title"
app:wheel_itemTextSize="@dimen/sp_16"
app:wheel_visibleItemCount="9" />
</LinearLayout>
</LinearLayout>
使用方式
早期省市区数据使用的是接口请求的方式,后期沟通过后讲返回数据做了本地处理,直接读本地json就好,有需要可直接前往 → Android进阶 - 存、取、读 本地 Json 文件
伪代码
binding.csCityParent.id -> {
//后台返回的省、市、区名称
var addressPickerEntity = AddressPickerEntity()
addressPickerEntity.province = province
addressPickerEntity.city = city
addressPickerEntity.cityId = cityId
//读取本地省市区数据
localAddressList = JsonParseUtil.getLocalAddress(
FileUtil.readAssetsFile(this,"address.json")
) as ArrayList<AddressResponse>
//创建 - 我们自定义的选择器
var pickerAddressPopup = PickerAddressPopup(
this@PerfectionActivity, addressPickerEntity,
localAddressList,
object : SimpleCallBack<AddressPickerEntity> {
override fun call(info: AddressPickerEntity) {
//自行操作选中的省、市、区即可
binding.tvAddress.text = info.province + info.city
province = info.province
city = info.city
cityId = info.cityId
}
})
//弹出 - 我们自定义的选择器
pickerAddressPopup.showPopup()
}