当前位置: 首页 > article >正文

重学 Android 自定义 View 系列(九):侧边字母选择器

前言

本文来实现一个侧边字母选择器,很常见的一个控件,和上篇文章星星评分用到的关键技术点类似,难度不大,本篇再来温故知新一下。

最终效果如下:
在这里插入图片描述

1. 效果分析


  • 每个字母被均匀分布在整个控件区域中;
  • 触摸某个字母时,该字母的颜色会改变,表示该字母被选中;
  • 支持字母的触摸反馈,提供字母选中时的提示。

2. 技术实现


AlphabetTouchView 的实现主要包括这几个方面:属性配置字母绘制触摸事件回调接口

2.1 属性配置

res/values/attrs.xml可配置字符大小、颜色、选中颜色,alphabet 为侧边字符默认为 "ABCDEFGHIJKLMNOPQRSTUVWXYZ#" 可自由配置。

<declare-styleable name="AlphabetTouchView">
    <attr name="textSize" format="dimension" />
    <attr name="textColor" format="color" />
    <attr name="selectedTextColor" format="color" />
    <attr name="alphabet" format="string" />
</declare-styleable>

通过 TypedArray 解析 XML 中配置的属性值

TypedArray a = context.getTheme().obtainStyledAttributes(
        attrs,
        R.styleable.AlphabetTouchView,
        0, 0);

try {
    mTextSize = a.getDimensionPixelSize(R.styleable.AlphabetTouchView_textSize, 14);
    mTextColor = a.getColor(R.styleable.AlphabetTouchView_textColor, Color.BLACK);
    mSelectedTextColor = a.getColor(R.styleable.AlphabetTouchView_selectedTextColor, Color.RED);
    mAlphabet = a.getString(R.styleable.AlphabetTouchView_alphabet);
    if (mAlphabet == null || mAlphabet.isEmpty()) {
        mAlphabet = DEFAULT_ALPHABET;
    }
} finally {
    a.recycle();
}

2.2 字母绘制

根据控件的高度和字母的个数,均匀分布字母,并确保字母在每个格子中垂直居中。
在这里插入图片描述

Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float fontHeight = fontMetrics.descent - fontMetrics.ascent;

for (int i = 0; i < mAlphabet.length(); i++) {
    float x = mWidth / 2 - mTextPaint.measureText(String.valueOf(mAlphabet.charAt(i))) / 2;
    float y = i * mLetterHeight + (mLetterHeight + fontHeight) / 2 - fontMetrics.descent;

    if (i == mSelectedIndex) {
        mTextPaint.setColor(mSelectedTextColor);
    } else {
        mTextPaint.setColor(mTextColor);
    }

    canvas.drawText(String.valueOf(mAlphabet.charAt(i)), x, y, mTextPaint);
}

看过前几篇文章的应该会发现,这次确定文字基线的方式和之前都不一样了,之前确定基线的两种方式分别是:

方式一:

     float dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
     float baseLine = getHeight() / 2 + dy;

方式二:

     float y = getHeight() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;

float y = i * mLetterHeight + (mLetterHeight + fontHeight) / 2 - fontMetrics.descent; 这种方式也能确定文字在格子中的基线并垂直居中显示,并不是那么好理解,在这里解释下:

  • i * mLetterHeight: 当前字母所在的格子的顶部位置
  • (mLetterHeight + fontHeight) / 2 : 计算字母在格子中应该偏移多少才能垂直居中
  • - fontMetrics.descent : 确保基线(baseline)正确对齐,fontMetrics.descent是从基线到字母底部的距离,所以减去这一部分能使得字母真正的中心对齐到格子的中心。

仔细观察下就理解了,到这里已经有三种方法确定基线的方式了,记住一个就行了。

2.3 触摸事件处理

onTouchEvent 方法中,根据触摸位置(event.getY())计算触摸的字母索引,并更新选中的字母索引 mSelectedIndex

@Override
public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
    float touchY = event.getY();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_MOVE:
            int index = (int) (touchY / (mHeight / mAlphabet.length()));
            if (index >= 0 && index < mAlphabet.length()) {
                mSelectedIndex = index;
                if (mListener != null) {
                    mListener.onLetterSelected(String.valueOf(mAlphabet.charAt(mSelectedIndex)), true);
                }
                invalidate();
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mListener != null) {
                mListener.onLetterSelected(String.valueOf(mAlphabet.charAt(mSelectedIndex)), false);
            }
            mSelectedIndex = -1;
            invalidate();
            break;
    }
    return true;
}

2.4 回调接口

通过 OnLetterSelectedListener 接口,向外部组件传递选中的字母,外部可以自由处理事件,如放大显示选中字符,滚动列表等。

public interface OnLetterSelectedListener {
    void onLetterSelected(String letter, boolean isTouch);
}

3. 完整代码


public class AlphabetTouchView extends View {
    private static final String DEFAULT_ALPHABET  = "ABCDEFGHIJKLMNOPQRSTUVWXYZ#";
    private String mAlphabet;
    private Paint mTextPaint;
    private Paint mBubblePaint;
    private int mTextSize;
    private int mTextColor;
    private int mSelectedTextColor = Color.RED;
    private int mWidth, mHeight;
    private float mLetterHeight;
    private int mSelectedIndex = -1;

    private OnLetterSelectedListener mListener;

    public void setOnLetterSelectedListener(OnLetterSelectedListener listener) {
        mListener = listener;
    }


    public AlphabetTouchView(Context context) {
        this(context, null);
    }

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

    public AlphabetTouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.AlphabetTouchView,
                0, 0);

        try {
            mTextSize = a.getDimensionPixelSize(R.styleable.AlphabetTouchView_textSize, 14);
            mTextColor = a.getColor(R.styleable.AlphabetTouchView_textColor, Color.BLACK);
            mSelectedTextColor = a.getColor(R.styleable.AlphabetTouchView_selectedTextColor, Color.RED);
            mAlphabet = a.getString(R.styleable.AlphabetTouchView_alphabet);
            if (mAlphabet == null || mAlphabet.isEmpty()) {
                mAlphabet = DEFAULT_ALPHABET;
            }
        } finally {
            a.recycle();
        }


        init();
    }

    private float sp2px(int sp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    private void init() {
        mTextPaint = new Paint();
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(sp2px(mTextSize));
        mTextPaint.setAntiAlias(true);

        mBubblePaint = new Paint();
        mBubblePaint.setColor(Color.parseColor("#AA0000"));
        mBubblePaint.setAntiAlias(true);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mLetterHeight = (float) mHeight / mAlphabet.length();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 绘制26个字母

        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); // 获取字体的度量信息
        float fontHeight = fontMetrics.descent - fontMetrics.ascent; // 字体高度

        for (int i = 0; i < mAlphabet.length(); i++) {
            // 计算字母的x坐标,使字母居中 ,x = mWidth / 2 - 文字的宽度 / 2
            float x = mWidth / 2 - mTextPaint.measureText(String.valueOf(mAlphabet.charAt(i))) / 2;

            // 计算字母的y坐标,使用均匀分布的间距
            float y = i * mLetterHeight + (mLetterHeight + fontHeight) / 2 - fontMetrics.descent;

            if (i == mSelectedIndex) {
                // 选中的字母文字颜色设置为选中颜色
                mTextPaint.setColor(mSelectedTextColor);
            } else {
                // 否则,使用普通的文字颜色
                mTextPaint.setColor(mTextColor);
            }

            // 绘制字母
            canvas.drawText(String.valueOf(mAlphabet.charAt(i)), x, y, mTextPaint);
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        float touchY = event.getY();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                // 根据触摸的Y坐标来计算字母的索引
                // 获取触摸点所在的字母
                int index = (int) (touchY / (mHeight / mAlphabet.length()));
                if (index >= 0 && index < mAlphabet.length()) {
                    mSelectedIndex = index;
                    if (mListener != null) {
                        mListener.onLetterSelected(String.valueOf(mAlphabet.charAt(mSelectedIndex)),true);
                    }
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mListener != null) {
                    mListener.onLetterSelected(String.valueOf(mAlphabet.charAt(mSelectedIndex)),false);
                }
                mSelectedIndex = -1;
                invalidate();
                break;
        }
        return true;
    }

    public interface OnLetterSelectedListener {
        void onLetterSelected(String letter,boolean  isTouch);
    }


}

4. 最后


难度不大,前面文章和接下来几篇文章都是打基础的,只有基础打牢了,遇到高级View才能不发怵,🤭。再会!

源码及更多自定义View已上传Github:DiyView

另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai


http://www.kler.cn/a/415004.html

相关文章:

  • c++设计模式模块与系统
  • 机器学习之RLHF(人类反馈强化学习)
  • 11.22Pytorch_自动微分
  • LeetCode 2290. Minimum Obstacle Removal to Reach Corner
  • vue3+ant design vue实现上传组件图片默认展示
  • 技能之发布自己的依赖到npm上
  • 生成二维码vue2
  • java全栈day10--后端Web基础(基础知识)之续集
  • 贵阳思普信息技术有限公司 OA系统 apilogin 接口存在SQL注入漏洞风险
  • 如何利用Java爬虫按关键字搜索工厂数据
  • JVM的内存区域划分
  • 【前端知识】SCSS(Sassy CSS)是一种CSS预处理器语言
  • 《Learn Three.js》学习(2)构建Three.js基本组件
  • 专业学习|如何绘制算法流程图?
  • 华为E9000刀箱(HWE9000V2)服务器硬件监控指标解读
  • http的文件上传和下载原理
  • 什么是C++中的函数对象?
  • 【二分查找】力扣 34. 在排序数组中查找元素的第一个和最后一个位置
  • 鸿蒙多线程应用-taskPool
  • spark3.x之后时间格式数据偶发报错org.apache.spark.SparkUpgradeException