【CustomView】封装下拉菜单组件(可设置阴影/宽高)- DropdownMenu

### 自定义下拉菜单组件 - 封装: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

欢迎指导...

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Android中使用Canvas绘制指定位置和度的图片,可以按照以下步骤进行: 1. 在XML布局文件中添加一个自定义的View,用于绘制图片: ```xml <com.example.myapplication.CustomView android:id="@+id/customView" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 2. 创建一个自定义的View类,继承自View,并重写其onDraw方法: ```java public class CustomView extends View { private Paint paint; private Bitmap bitmap; public CustomView(Context context) { super(context); init(); } public CustomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { paint = new Paint(); bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.your_image); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制图片 // 指定位置和 int left = 100; int top = 100; int right = left + 200; int bottom = top + 200; Rect rect = new Rect(left, top, right, bottom); canvas.drawBitmap(bitmap, null, rect, paint); } } ``` 3. 在Activity中找到该自定义View,并设置为ContentView: ```java public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CustomView customView = findViewById(R.id.customView); } } ``` 这样就可以在指定位置和度上绘制图片了。记得将 `R.drawable.your_image` 替换为你自己的图片资源ID。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值