Android 自定义View实现 Spinner

转载请注明出处:http://blog.csdn.net/a512337862/article/details/79379812

最近,因为项目中需要用到大量的下拉选择框,Spinner无法满足需求,用Layout+ImageButton又太多重复的工作,所以自己写了一个自定义View实现Spinner效果,主要功能包括:设置边框颜色/圆角/宽度,添加Spinner标题(左侧的提示文字),设置下拉按钮/下拉按下按钮图片等。效果图如下:

这里写图片描述

思路

1.根据是否存在title来判断是否需要绘制左侧的title文本
2.绘制最外层的边框以及下拉按钮图片(支持svg)
3.绘制选中的文本
4.通过OnTouchListener+PopupWindow+ListView实现下拉框

代码实现

自定义属性

关于自定义属性,无太多可说的,简单贴一下代码:

attr.xml

<!--SpinnerView-->
    <declare-styleable name="SpinnerView">
        <!--下拉按钮资源-->
        <attr name="pullDownSrc" format="reference" />
        <!--下拉按下按钮资源-->
        <attr name="pullDownPressedSrc" format="reference" />
        <!--字体大小-->
        <attr name="textSize"/>
        <!--字体颜色-->
        <attr name="textColor"  />
        <!--边框颜色-->
        <attr name="borderColor" />
        <!--边框宽度-->
        <attr name="borderWidth"/>
        <!--按钮资源padding-->
        <attr name="srcPadding" format="dimension"/>
        <!--标题大小-->
        <attr name="titleSize" format="dimension"/>
        <!--标题颜色-->
        <attr name="titleColor" format="color" />
        <!--标题-->
        <attr name="title" format="string" />
        <!--输入框大小-->
        <attr name="inputLen" format="dimension"/>
        <!--圆角度-->
        <attr name="radius" format="dimension"/>
    </declare-styleable>

代码中获取自定义属性

TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SpinnerView, defStyleAttr, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.SpinnerView_pullDownPressedSrc:
                    //下拉按钮按下资源ID
                    pullDownPressedSrc = a.getResourceId(attr, R.drawable.pull_down_pressed);
                    break;
                case R.styleable.SpinnerView_pullDownSrc:
                    //下拉按钮资源ID
                    pullDownSrc = a.getResourceId(attr, R.drawable.pull_down);
                    break;
                case R.styleable.SpinnerView_textSize:
                    // 默认设置为16sp,TypeValue也可以把sp转化为px
                    textSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_textColor:
                    //字体颜色
                    textColor = a.getColor(attr, Color.BLUE);
                    break;
                case R.styleable.SpinnerView_borderWidth:
                    // 默认设置为2px,TypeValue也可以把sp转化为px
                    borderWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_borderColor:
                    //边框颜色
                    borderColor = a.getColor(attr, Color.GRAY);
                    break;
                case R.styleable.SpinnerView_srcPadding:
                    // 默认设置为2px,TypeValue也可以把sp转化为px
                    srcPadding = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_titleSize:
                    // 默认设置为16sp,TypeValue也可以把sp转化为px
                    titleSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_titleColor:
                    //字体颜色
                    titleColor = a.getColor(attr, Color.BLUE);
                    break;
                case R.styleable.SpinnerView_title:
                    //字体颜色
                    title = a.getString(attr);
                    break;
                case R.styleable.SpinnerView_inputLen:
                    inputLen = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_radius:
                    radius = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()));
                    break;
                default:
                    break;
            }
        }
        a.recycle();

SpinnerView

先贴一下完整的代码,然后在介绍比较关键的代码:

/**
 * Author : BlackHao
 * Time : 2018/2/23 13:54
 * Description : 自定义 Spinner
 */

public class SpinnerView extends View implements View.OnTouchListener {

    //数据源
    private ArrayList<String> data;
    //需要显示的数据的下标
    private int showStringIndex = 0;
    //边框颜色
    private int borderColor;
    //边框宽度
    private int borderWidth;
    //下拉按钮资源ID
    private int pullDownSrc;
    //下拉按钮按下资源ID
    private int pullDownPressedSrc;
    //下拉按钮Bitmap
    private Bitmap pullDownBitmap;
    //下拉按钮按下Bitmap
    private Bitmap pullDownPressedBitmap;
    //padding
    private int srcPadding;
    //按钮显示的边长
    private int sideLength;
    //字体颜色
    private int textColor;
    //字体大小
    private int textSize;
    //边框圆角
    private int radius;
    //输入框长度
    private int inputLen;
    //标题
    private String title;
    //标题颜色
    private int titleColor;
    //标题大小
    private int titleSize;
    //画笔
    private Paint paint;
    //是否按下标识
    private boolean isPressed = false;
    //
    private Rect rect;
    //选择框
    private PopupWindow pop;


    public SpinnerView(Context context) {
        this(context, null, 0);
    }

    public SpinnerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SpinnerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //默认参数
        initDefaultValue();
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SpinnerView, defStyleAttr, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.SpinnerView_pullDownPressedSrc:
                    //下拉按钮按下资源ID
                    pullDownPressedSrc = a.getResourceId(attr, R.drawable.pull_down_pressed);
                    break;
                case R.styleable.SpinnerView_pullDownSrc:
                    //下拉按钮资源ID
                    pullDownSrc = a.getResourceId(attr, R.drawable.pull_down);
                    break;
                case R.styleable.SpinnerView_textSize:
                    // 默认设置为16sp,TypeValue也可以把sp转化为px
                    textSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_textColor:
                    //字体颜色
                    textColor = a.getColor(attr, Color.BLUE);
                    break;
                case R.styleable.SpinnerView_borderWidth:
                    // 默认设置为2px,TypeValue也可以把sp转化为px
                    borderWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_borderColor:
                    //边框颜色
                    borderColor = a.getColor(attr, Color.GRAY);
                    break;
                case R.styleable.SpinnerView_srcPadding:
                    // 默认设置为2px,TypeValue也可以把sp转化为px
                    srcPadding = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_titleSize:
                    // 默认设置为16sp,TypeValue也可以把sp转化为px
                    titleSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_titleColor:
                    //字体颜色
                    titleColor = a.getColor(attr, Color.BLUE);
                    break;
                case R.styleable.SpinnerView_title:
                    //字体颜色
                    title = a.getString(attr);
                    break;
                case R.styleable.SpinnerView_inputLen:
                    inputLen = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SpinnerView_radius:
                    radius = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()));
                    break;
                default:
                    break;
            }
        }
        a.recycle();
        //初始化画笔
        paint = new Paint();
        paint.setAntiAlias(true);

        rect = new Rect();
        //初始化Bitmap
        pullDownBitmap = BitmapFactory.decodeResource(getResources(), pullDownSrc);
        pullDownPressedBitmap = BitmapFactory.decodeResource(getResources(), pullDownPressedSrc);
        //转换失败
        if (pullDownBitmap == null) {
            pullDownBitmap = getBitmapFromVectorDrawable(pullDownSrc);
        }
        if (pullDownPressedBitmap == null) {
            pullDownPressedBitmap = getBitmapFromVectorDrawable(pullDownPressedSrc);
        }
        //设置点击监听
        setOnTouchListener(this);
    }

    //初始化默认参数
    private void initDefaultValue() {
        textSize = 20;
        textColor = Color.GRAY;
        title = "";
        titleSize = 20;
        titleColor = Color.BLACK;
        srcPadding = 2;
        sideLength = 20;
        borderWidth = 3;
        borderColor = Color.GRAY;
        pullDownPressedSrc = R.drawable.pull_down_pressed;
        pullDownSrc = R.drawable.pull_down;
        inputLen = 50;
        radius = 0;
        data = new ArrayList<>();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //输入框以外的长度
        int otherLen;
        //绘制标题
        if (!title.equals("") && title.length() > 0) {
            paint.setColor(titleColor);
            paint.setTextSize(titleSize);
            paint.setStyle(Paint.Style.FILL);
            rect.set(10, 0, getTitleLength(title), getHeight());
            drawTextOnRect(canvas, rect, title);
            otherLen = getWidth() - inputLen;
        } else {
            otherLen = 0;
        }
        //绘制边框
        paint.setStrokeWidth(borderWidth);
        paint.setColor(borderColor);
        paint.setStyle(Paint.Style.STROKE);

        canvas.drawRoundRect(otherLen + borderWidth / 2, borderWidth / 2,
                getWidth() - borderWidth / 2, getHeight() - borderWidth / 2, radius, radius, paint);
        //绘制按钮
        sideLength = getHeight() - srcPadding * 2;
        rect.set(getWidth() - sideLength - srcPadding, srcPadding,
                getWidth() - srcPadding, getHeight() - srcPadding);
        if (isPressed) {
            canvas.drawBitmap(pullDownPressedBitmap, null, rect, paint);
        } else {
            canvas.drawBitmap(pullDownBitmap, null, rect, paint);
        }
        //绘制选中的文字
        paint.setStyle(Paint.Style.FILL);
        rect.set(otherLen, 0, getWidth() - sideLength, getHeight());
        if (data != null && data.size() > 0) {
            paint.setColor(textColor);
            paint.setTextSize(textSize);
            drawTextOnRect(canvas, rect, data.get(showStringIndex));
        }
    }

    /**
     * 在指定矩形中间drawText
     *
     * @param canvas     画布
     * @param targetRect 指定矩形
     * @param text       需要绘制的Text
     */
    private void drawTextOnRect(Canvas canvas, Rect targetRect, String text) {
        Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
        // 获取baseLine
        int baseline = targetRect.top + (targetRect.bottom - targetRect.top - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
        // 下面这行是实现水平居中,drawText对应改为传入targetRect.centerX()
        paint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText(text, targetRect.centerX(), baseline, paint);
    }

    public int getTitleLength(String title) {
        Rect rect = new Rect();
        paint.getTextBounds(title, 0, title.length(), rect);
        return rect.width();//文本的宽度
    }

    //设置数据源
    public void setData(ArrayList<String> data) {
        this.data.clear();
        this.data.addAll(data);
        postInvalidate();
    }

    //设置当前显示的数据在数据源的下标
    public void setShowStringIndex(int showStringIndex) {
        this.showStringIndex = showStringIndex;
    }

    //初始化popUpWindow
    private void initPop() {
        //显示ratioPop
        View view = LayoutInflater.from(getContext()).inflate(R.layout.pop_layout, null);
        //通过是否存在title判断pop的宽度
        int width = (!title.equals("") && title.length() > 0) ? inputLen : getWidth();
        //设置pop的宽度,数据长度大于5时,显示5个item的高度,否则显示所有item的高度
        int height = data.size() > 5 ? dp2px(getContext(), 30) * 5 : dp2px(getContext(), 30) * data.size();
        pop = new PopupWindow(view, width - getHeight(), height);
        ListView listView = (ListView) view.findViewById(R.id.pop_list_view);
        ListAdapter arrayAdapter = new ListAdapter();
        listView.setAdapter(arrayAdapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                showStringIndex = position;
                postInvalidate();
                pop.dismiss();
            }
        });
        pop.setOutsideTouchable(false);
    }

    //显示popUpWindow
    private void showPop() {
        if (pop == null) {
            initPop();
        }
        if (pop.isShowing()) {
            pop.dismiss();
        } else {
            if (!title.equals("") && title.length() > 0) {
                pop.showAsDropDown(this, getWidth() - inputLen, 0);
            } else {
                int xPos = -pop.getWidth() / 2 + pop.getWidth() / 2;
                pop.showAsDropDown(this, xPos, 0);
            }
        }
    }

    //返回选中的字符
    public String getText() {
        return data.get(showStringIndex);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //计算View的宽高
        int width = widthSize, height = heightSize;
        if (widthMode == MeasureSpec.EXACTLY) {
            //指定大小或者match_parent
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            //wrap_content
            width = 300;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            //指定大小或者match_parent
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            //wrap_content
            height = 50;
        }
        //设置按钮图片的边长
        setMeasuredDimension(width, height);
    }

    //从矢量图获取Bitmap
    public Bitmap getBitmapFromVectorDrawable(int drawableId) {
        Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            drawable = (DrawableCompat.wrap(drawable)).mutate();
        }
        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
                Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }

    /**
     * 将dp转换为与之相等的px
     */
    public static int dp2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isPressed = true;
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
                isPressed = false;
                postInvalidate();
                showPop();
                break;
        }
        return true;
    }

    private class ListAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return data.size();
        }

        @Override
        public Object getItem(int position) {
            return data.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
            if (convertView == null) {
                convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_spinner_list, parent, false);
                holder = new ViewHolder(convertView);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.tv.setText(data.get(position));
            return convertView;
        }

        class ViewHolder {
            TextView tv;
            ViewHolder(View v) {
                this.tv = (TextView) v.findViewById(R.id.spinner_list_tv);
            }
        }
    }
}

1.在构造方法中,下面这段代码是获取Bitmap用于绘制下拉按钮,这里先利用BitmapFactory来转换,转化失败表示不是JPG或者PNG图片。如果转换失败,再尝试从矢量图获取Bitmap。

 //初始化Bitmap
        pullDownBitmap = BitmapFactory.decodeResource(getResources(), pullDownSrc);
        pullDownPressedBitmap = BitmapFactory.decodeResource(getResources(), pullDownPressedSrc);
        //转换失败
        if (pullDownBitmap == null) {
            pullDownBitmap = getBitmapFromVectorDrawable(pullDownSrc);
        }
        if (pullDownPressedBitmap == null) {
            pullDownPressedBitmap = getBitmapFromVectorDrawable(pullDownPressedSrc);
        }

2.inputLen表示输入框(既需要绘制边框的部分)的长度,这里有两种情况:
a.当标题为空时,inputLen可有可无。
b.当标题不为空时,iputLen则必须设置。

3.初始化PopupWindow时,需要判断PopupWindow的宽高:

//显示ratioPop
        View view = LayoutInflater.from(getContext()).inflate(R.layout.pop_layout, null);
        //通过是否存在title判断pop的宽度
        int width = (!title.equals("") && title.length() > 0) ? inputLen : getWidth();
        //设置pop的宽度,数据长度大于5时,显示5个item的高度,否则显示所有item的高度
        int height = data.size() > 5 ? dp2px(getContext(), 30) * 5 : dp2px(getContext(), 30) * data.size();
        pop = new PopupWindow(view, width - getHeight(), height);

width 分两种情况,当title存在和不存在,PopupWindow的宽度不一样。height则需要根据需要显示的数据长度判读,数据长度大于5时,显示5个item的高度,否则显示所有item的高度。

4.ListView的适配器这里是自定义的,方便后面修改ListView Item的内容。嫌麻烦也可以直接用ArrayAdapter。

结语

1.源码github地址 : https://github.com/LuoChen-Hao/BlackHaoCustomView

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现 Android Kotlin 中按钮展示 Spinner自定义样式,可以按照以下步骤进行: 1. 在布局文件中添加一个 Button 控件和一个隐藏的 Spinner 控件: ```xml <Button android:id="@+id/btn_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Select Item"/> <Spinner android:id="@+id/spinner" android:layout_width="0dp" android:layout_height="0dp" android:visibility="gone"/> ``` 2. 在 Kotlin 代码中设置 Button 的点击事件: ```kotlin btn_spinner.setOnClickListener { spinner.performClick() } ``` 3. 创建一个自定义Spinner 样式,例如: ```xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="16dp"> <ImageView android:id="@+id/img_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher"/> <TextView android:id="@+id/tv_item_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:text="Item Name" android:textSize="16sp"/> </LinearLayout> ``` 4. 创建一个自定义Spinner 适配器: ```kotlin class CustomSpinnerAdapter(private val context: Context, private val items: List<String>) : ArrayAdapter<String>(context, 0, items) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { var view = convertView if (view == null) { view = LayoutInflater.from(context).inflate(R.layout.item_spinner, parent, false) } val tvItemName = view?.findViewById<TextView>(R.id.tv_item_name) tvItemName?.text = items[position] return view!! } override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { var view = convertView if (view == null) { view = LayoutInflater.from(context).inflate(R.layout.item_spinner_dropdown, parent, false) } val tvItemName = view?.findViewById<TextView>(R.id.tv_item_name) tvItemName?.text = items[position] return view!! } } ``` 5. 在 Kotlin 代码中设置 Spinner 的适配器和选择事件: ```kotlin val items = listOf("Item 1", "Item 2", "Item 3") val adapter = CustomSpinnerAdapter(this, items) spinner.adapter = adapter spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { btn_spinner.text = items[position] } override fun onNothingSelected(parent: AdapterView<*>?) { // Do nothing } } ``` 这样就可以实现一个自定义样式的 Spinner,展示在一个 Button 控件上。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值