### 自定义下拉菜单组件 - 封装:TextView来显示初始状态 + PopupWindow(确定弹出位置) + CardView(设置弹框背景阴影效果)+ RecyclerView(数据列表显示)+ 可设置宽高
Android 有自带的下拉菜单组件 Spinner, 在横屏的状态下,使用Spinner会存在一个小问题:
在pad设备的横屏状态中,如果Spinner设置了popupBackground,则在弹出ListPupopWindow时会闪烁一个额外的视图(可能只是某一帧画面,可能有像素眼才能看到,我是录屏 转成gif格式后,一帧一帧的翻到的)。
***此缺陷仅在设备处于横向模式时发生。 并且此缺陷不会出现在垂直屏幕中。
如下图所示:
先附上效果:
再大致说下我的思路吧:
#1:下拉菜单,这里用简单PopupWindow来显示它,而且它弹出来的位置可以自定义:
fun show(
anchorView: View,
selectPosition: Int = -1,
mode: DropDownMode = DropDownMode.DROPDOWN_MODE_DEFAULT,
innerOffsetX: Int = 0, //用来计算位置的偏移量的
innerOffsetY: Int = 0 //用来计算位置的偏移量的
) {
val location = getLocationOnScreen(anchorView, context)
val anchorRect = Rect(
location.x, location.y,
location.x + anchorView.width,
location.y + anchorView.height
)
val positionX: Int
val positionY: Int
when (mode) {
DropDownMode.DROPDOWN_MODE_CENTER -> { // 看这里,居中
positionX = anchorRect.left - dropDownMenuWindow.width / 2
positionY = anchorRect.top - dropDownMenuWindow.height / 2
}
DropDownMode.DROPDOWN_MODE_DEFAULT -> { // 看这里,位于点击文本的后下方
positionX = anchorRect.left - innerOffsetX // innerOffsetX = Shadow + margin
positionY = anchorRect.top - innerOffsetY
}
}
dropDownMenuWindow.showAtLocation(anchorView, Gravity.NO_GRAVITY, positionX, positionY)
}
#2:在考虑到需要阴影,本来直接用PopupWindow,背景设置圆角白底的,然后额外的部分设置成透明,会发现不存在阴影,所以想到了CardView,但是,如果弹出框的大小和CardView的大小一直,也是不存在阴影的,所以这里设置了额外的margin:
//不要用这里的代码,后面会附上完整的代码
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="25dp" // 看这里
android:background="@color/primary_white"
android:outlineSpotShadowColor="@color/primary_black"
android:visibility="visible"
app:cardCornerRadius="12dp"
app:cardElevation="10dp">
此时的阴影会显示在margin的25dp中。
#3:接下来就是 展示数据了 - RecyclerView,这里我用的泛型,为了更好的适配所以的数据类型显示,
class DropDownMenuAdapter<T>(
val list: MutableList<T>
) : DropDownBaseAdapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AdminViewHolder {
return AdminViewHolder(parent.inflate(R.layout.drop_down_menu_item))
}
override fun onBindViewHolder(holder: DropDownBaseViewHolder, position: Int) {
holder.bind(position, list[position])
}
override fun getItemCount(): Int {
return list.size
}
inner class AdminViewHolder(view: View) : DropDownBaseViewHolder(view) {
override fun <T> bind(position: Int, item: T) {
with(itemView) {
if (selectPosition == position) {
ic_check?.show()
} else {
ic_check?.invisible()
}
// 因为本 Demo 中传入数组是 MutableList<String> 类型,如果传入的是MutableList<对象>,这里需要根据类型来显示相应的数据
// title.text = item.toString()
// 这里是为了更好的自定义显示,用回调去适配不同 T (对象)
onItemDisplay?.invoke(title, position) // 显示下拉菜单选项的文本
select_item.setOnClickListener {
displayText = title.text.toString()
onItemSelected?.invoke(position)
}
}
}
}
}
#4:运行后发现在菜单列表文本长度不一致的时候,导致宽度不会适应整个列表中最长的字符,只会适应当前显示的选项中最长的字符。
在基于手头的功能,所以我这里就自己来设置宽度:(简单粗暴直接,如果有更好的办法,望指导...)
with(dropDownMenuWindow) {
contentView = contentLayout
width = if (builder.optionsWidth > 0) {
setDropDownWidth(builder.optionsWidth)
} else {
WindowManager.LayoutParams.WRAP_CONTENT
}
height =
if (builder.visibleOptions > 0 && contentLayout.recycler_view.layoutManager?.itemCount ?: 0 > builder.visibleOptions) {
setDropDownHeight(builder.visibleOptions * builder.itemHeight)
} else {
WindowManager.LayoutParams.WRAP_CONTENT
}
setOnDismissListener {
onDismiss?.invoke()
}
}
### 补充: 这里我去看了Spinner的实现,它也是适当的去循环查看列表中最长的字符,然后取最大的宽度设置成弹出框的宽度,代码如下:
int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
if (adapter == null) {
return 0;
}
int width = 0;
View itemView = null;
int itemType = 0;
final int widthMeasureSpec =
MeasureSpec.makeSafeMeasureSpec(getMeasuredWidth(), MeasureSpec.UNSPECIFIED);
final int heightMeasureSpec =
MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED);
// Make sure the number of items we'll measure is capped. If it's a huge data set
// with wildly varying sizes, oh well.
int start = Math.max(0, getSelectedItemPosition());
final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
final int count = end - start;
start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
for (int i = start; i < end; i++) {
final int positionType = adapter.getItemViewType(i);
if (positionType != itemType) {
itemType = positionType;
itemView = null;
}
itemView = adapter.getView(i, itemView, this);
if (itemView.getLayoutParams() == null) {
itemView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
}
itemView.measure(widthMeasureSpec, heightMeasureSpec);
width = Math.max(width, itemView.getMeasuredWidth());
}
// Add background padding to measured width
if (background != null) {
background.getPadding(mTempRect);
width += mTempRect.left + mTempRect.right;
}
return width;
}
#5: 在不设置高度的时候,下拉菜单列表选项数据过多时,整个列表的位置会整体展开,充满屏幕,这样的效果显然不美观,UI也不会同意的,这里我设置成可见选项数来设置下拉菜单的高度;
这里附上如何设置 Spinner 的弹出高度 >>> LimitPopupHeightSpinner (有需要的可以去看下)
#6:为了封装的的彻底,我也把入口TextView封装进来,简单理解为:点击的显示选择后文本的地方,所以使用很简单:
XML:
<com.android.dropdownmenu.dropdown.DropDownLayoutView
android:id="@+id/drop_down_view"
android:layout_width="200dp"
android:layout_height="40dp"
android:layout_marginStart="70dp"
android:layout_marginEnd="30dp"
app:innerTextColor="@color/primary_black" /* 下拉菜单中设置显示的字体颜色 */
app:innerTextSize="14sp" /* 下拉菜单中设置显示的字体大小 */
app:innerOffsetX="45dp" /* 下拉菜单X轴偏移距离,用来调整弹出位置 */
app:innerOffsetY="25dp" /* 下拉菜单Y轴偏移距离,用来调整弹出位置 */
app:innerMenuWidth="300dp" /* 下拉菜单宽度 */
app:innerVisibleOptions="3" /* 下拉菜单列表可见项,即设置该项可以控制下拉菜单的高度 */
app:innerItemHeight="60dp" /* 下拉菜单列表单项的高度 */
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_bottom" />
代码中:
drop_down_view.setDropDownAdapter(
list = testList, /* 下拉菜单选项的数据 */
preText = "请选择", /* 预显示文本 */
autoSelected = false, /* 是否存在默认选择 */
autoSelectedPosition = preIndex2, /* 存在默认选择的位置 */
onSelectionDisplay = { selection: TextView, i: Int -> /* 显示下拉菜单选项的文本 */
// 显示下拉菜单选项的文本,放到这里是为了更好的支持任意对象的显示
selection.text = testList[i] // testList是 String类型
},
onSelected = { /* 下拉菜单选中后回调 */
if (it != -1) {
Toast.makeText(this, "你选择了"+ testList[it], Toast.LENGTH_LONG).show()
}
},
options = 2 /* 下拉菜单列表可见项,即设置该项可以控制下拉菜单的高度 */
)
完整代码附上:DropdownMenu
欢迎指导...