android最简单的自定义控件_Android自定义简单字母索引栏

说到字母索引栏,在联系人模块经常会见到。iOS中这种控件是自带的,安卓中是没有的,所以只能自定义解决了。
需求分析
  1. 拖动索引栏时。选中状态

  • 字母栏背景加深

  • 文字颜色变白色

  • 悬浮字母提示显示

松手时,非选中状态

  • 字母栏背景透明

  • 文字颜色变黑色

  • 悬浮字母提示隐藏

需要自定义的属性
  1. 选中时字体颜色

  2. 未选中时字体颜色

  3. 选中时,索引栏背景颜色

  4. 非选中时,索引栏背景颜色

提供的回调
选中和未选中时,回调位置和字母文字。
索引条定义步骤(注释上已经写得很清楚,就不再一步步赘述了)
  • 自定义需要的属性

<declare-styleable name="SlideBar">        <attr name="slb_select_txt_color" format="color|reference" />        <attr name="slb_un_select_txt_color" format="color|reference" />        <attr name="slb_select_bg_color" format="color|reference" />        <attr name="slb_un_select_bg_color" format="color|reference" />declare-styleable>
  • 自定义View,继承View,获取设置的自定义属性,配置画笔等

public class SlideBar extends View {    /**     * 默认选中时,文字颜色     */    private final int mDefaultSelectTextColor = Color.parseColor("#FFFFFF");    /**     * 默认未选中时,文字颜色     */    private final int mDefaultUnSelectTextColor = Color.parseColor("#202020");    /**     * 默认选中时,滑动条背景颜色     */    private final int mDefaultSelectBgColor = Color.parseColor("#66202020");    /**     * 默认未选中时,滑动条背景颜色     */    private final int mDefaultUnSelectBgColor = Color.parseColor("#00000000");         /**     * 字母表     */    private String[] mLetter = new String[]{"#", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};        /**     * 是否拖动索引条中     */    private boolean mTouched = false;         /**     * 画笔     */    private Paint mPaint;    private int mSelectTextColor;    private int mUnSelectTextColor;    private int mSelectBgColor;    private int mUnSelectBgColor;        public SlideBar(Context context) {        super(context);        init(context, null);    }    public SlideBar(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        init(context, attrs);    }    public SlideBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init(context, attrs);    }    private void init(Context context, @Nullable AttributeSet attrs) {        initAttrs(context, attrs);        initPaint();    }    private void initAttrs(Context context, @Nullable AttributeSet attrs) {        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SlideBar);        //选中时文字的颜色        mSelectTextColor = array.getColor(R.styleable.SlideBar_slb_select_txt_color, mDefaultSelectTextColor);        //未选中时文字的颜色        mUnSelectTextColor = array.getColor(R.styleable.SlideBar_slb_un_select_txt_color, mDefaultUnSelectTextColor);        //选中时,滑动条背景颜色        mSelectBgColor = array.getColor(R.styleable.SlideBar_slb_select_bg_color, mDefaultSelectBgColor);        //未选中时,滑动条背景颜色        mUnSelectBgColor = array.getColor(R.styleable.SlideBar_slb_un_select_bg_color, mDefaultUnSelectBgColor);        array.recycle();    }    private void initPaint() {        mPaint = new Paint();        mPaint.setTextSize(sp2px(getContext(), 11f));        if (mTouched) {            mPaint.setColor(mSelectTextColor);        } else {            mPaint.setColor(mUnSelectTextColor);        }        mPaint.setAntiAlias(true);        mPaint.setStyle(Paint.Style.STROKE);        mTextRect = new Rect();    }}
  • 定义回调接口

public interface OnSelectItemListener {    /**     * 选择时回调     *     * @param position     选中的位置     * @param selectLetter 选中的字母     */    void onItemSelect(int position, String selectLetter);    /**     * 松手取消选中时回调     */    void onItemUnSelect();}public void setOnSelectItemListener(OnSelectItemListener listener) {    mListener = listener;}
  • 配置测量

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    setMeasuredDimension(measureWidth(widthMeasureSpec), heightMeasureSpec);}/** * 测量本身的大小,这里只是测量宽度 * * @param widthMeaSpec 传入父View的测量标准 * @return 测量的宽度 */private int measureWidth(int widthMeaSpec) {    /*定义view的宽度*/    int width;    /*获取当前 View的测量模式*/    int mode = MeasureSpec.getMode(widthMeaSpec);    /*     * 获取当前View的测量值,这里得到的只是初步的值,     * 我们还需根据测量模式来确定我们期望的大小     * */    int size = MeasureSpec.getSize(widthMeaSpec);    /*     * 如果,模式为精确模式     * 当前View的宽度,就是我们的size     * */    if (mode == MeasureSpec.EXACTLY) {        width = size;    } else {        /*否则的话我们就需要结合padding的值来确定*/        int desire = size + getPaddingLeft() + getPaddingRight();        if (mode == MeasureSpec.AT_MOST) {            width = Math.min(desire, size);        } else {            width = desire;        }    }    return width;}
  • 获取到宽高时,计算每个字母的高度

@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {    super.onSizeChanged(w, h, oldw, oldh);    mWidth = w;    //每行的高度,计算平均分每个字母占的高度    mCellHeight = h / mLetter.length;}
  • 绘制文字和背景,触摸和松手时改变变量来达到绘制不同的颜色

@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    //触摸时改变背景颜色    if (mTouched) {        canvas.drawColor(mSelectBgColor);        mPaint.setColor(mSelectTextColor);    } else {        canvas.drawColor(mUnSelectBgColor);        mPaint.setColor(mUnSelectTextColor);    }    for (int i = 0; i < mLetter.length; i++) {        String text = mLetter[i];        //测量文字宽高        mPaint.getTextBounds(text, 0, text.length(), mTextRect);        int textWidth = mTextRect.width();        int textHeight = mTextRect.height();        //文字一半的宽度        float textHalfWidth = textWidth / 2.0f;        //字母文字的起点X坐标,控件的宽度的一半再减去文字的一半        float x = (mWidth / 2.0f) - textHalfWidth;        //起点文字的Y坐标        float y = (mCellHeight / 2.0f + textHeight / 2.0f + mCellHeight * i);        //画文字        canvas.drawText(mLetter[i], x, y, mPaint);    }}
  • 处理触摸和松手判断,并进行回调

@Overridepublic boolean onTouchEvent(MotionEvent event) {    float y = event.getY();    //计算当前触摸的字母的位置    int index = (int) (y / mCellHeight);    //触摸    if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {        mTouched = true;        if (index >= 0 && index < mLetter.length) {            if (mListener != null) {                mListener.onItemSelect(index, mLetter[index]);            }        } else {            return super.onTouchEvent(event);        }    } else {        //松手        mTouched = false;        if (mListener != null) {            mListener.onItemUnSelect();        }    }    //触摸改变时,不断通知重绘来绘制索引条    invalidate();    return true;}
界面布局
  • RecyclerView列表

  • 索引栏

  • 字母提示(我们直接用个TextView,设置背景,显示隐藏即可)

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context=".MainActivity">    <android.support.v7.widget.RecyclerView        android:id="@+id/refresh_list"        android:layout_width="match_parent"        android:layout_height="match_parent" />    <me.zh.indexslidebar.SlideBar        android:id="@+id/slide_bar"        android:layout_width="24dp"        android:layout_height="match_parent"        android:layout_gravity="end"        android:visibility="visible"        app:slb_select_bg_color="@android:color/darker_gray"        app:slb_select_txt_color="@android:color/white"        app:slb_un_select_bg_color="@android:color/transparent"        app:slb_un_select_txt_color="@color/colorPrimary" />    <TextView        android:id="@+id/check_letter"        android:layout_width="100dp"        android:layout_height="100dp"        android:layout_gravity="center"        android:background="@drawable/bg_contact_letter_flow"        android:gravity="center"        android:textColor="#FFFFFF"        android:textSize="40sp"        android:visibility="gone"        tools:text="A"        tools:visibility="visible" />FrameLayout>
具体使用
  • 提供一组联系人名字

/** * 联系人数组 */public static String[] mNames = new String[]{"宋江", "卢俊义", "吴用",        "公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进", "李应", "朱仝", "鲁智深",        "武松", "董平", "张清", "杨志", "徐宁", "索超", "戴宗", "刘唐", "李逵", "史进", "穆弘",        "雷横", "李俊", "阮小二", "张横", "阮小五", "张顺", "阮小七", "杨雄", "石秀", "解珍",        "解宝", "燕青", "朱武", "黄信", "孙立", "宣赞", "郝思文", "韩滔", "彭玘", "单廷珪",        "魏定国", "萧让", "裴宣", "欧鹏", "邓飞", "燕顺", "杨林", "凌振", "蒋敬", "吕方",        "郭 盛", "安道全", "皇甫端", "王英", "扈三娘", "鲍旭", "樊瑞", "孔明", "孔亮", "项充",        "李衮", "金大坚", "马麟", "童威", "童猛", "孟康", "侯健", "陈达", "杨春", "郑天寿",        "陶宗旺", "宋清", "乐和", "龚旺", "丁得孙", "穆春", "曹正", "宋万", "杜迁", "薛永", "施恩",        "周通", "李忠", "杜兴", "汤隆", "邹渊", "邹润", "朱富", "朱贵", "蔡福", "蔡庆", "李立",        "李云", "焦挺", "石勇", "孙新", "顾大嫂", "张青", "孙二娘", "王定六", "郁保四", "白胜",        "时迁", "段景柱", "&张三", "11级李四", "12级小明"};}
  1. 字母和字母条的位置映射

/** * 用于保存联系人首字母在列表的位置 */private HashMap<String, Integer> mLetterPositionMap = new HashMap<>();
  1. 配置好3个View(Rv使用自己熟悉的框架即可,这里使用的是MultiType)

public class MainActivity extends AppCompatActivity {    private RecyclerView vRefreshList;    /**     * 字母侧滑栏     */    private SlideBar mSlideBar;    /**     * 提示View     */    private TextView vCheckLetterView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        findView();        bindView();        setData();    }        private void findView() {        mSlideBar = findViewById(R.id.slide_bar);        vRefreshList = findViewById(R.id.refresh_list);        vCheckLetterView = findViewById(R.id.check_letter);    }    private void bindView() {        //配置列表        vRefreshList.setLayoutManager(new LinearLayoutManager(this));        mListItems = new Items();        mListAdapter = new MultiTypeAdapter(mListItems);        //联系人字母条目        mListAdapter.register(ContactLetterModel.class, new ContactLetterViewBinder());        //联系人条目        mListAdapter.register(ContactModel.class, new ContactViewBinder());        vRefreshList.setAdapter(mListAdapter);    }
  1. 配置索引条回调,在选中时,将列表滚动到记录的位置,有些字母在我们的字母表里面可能没有对应的,会找不到,所以需要判空

//配置索引条mSlideBar.setOnSelectItemListener(new SlideBar.OnSelectItemListener() {    @Override    public void onItemSelect(int position, String selectLetter) {        if (vCheckLetterView.getVisibility() != View.VISIBLE) {            vCheckLetterView.setVisibility(View.VISIBLE);        }        vCheckLetterView.setText(selectLetter);        Integer letterStickyPosition = mLetterPositionMap.get(selectLetter);        //这里可能拿不到,因为并不是所有的字母联系人名字上都有        if (letterStickyPosition != null) {            vRefreshList.scrollToPosition(letterStickyPosition);        }    }    @Override    public void onItemUnSelect() {        vCheckLetterView.setVisibility(View.GONE);    }});
  1. 根据姓名表,构造联系人列表和记录字母条位置条目,这里需要将姓名的第一个子的首字母,用到了一个拼音库。

implementation 'com.github.promeg:tinypinyin:2.0.3'
先来将姓名表按姓名的第一个字的英文字母来排序,这样是为了后面遍历判断字母,对同一组字母的条目的第一个条目插入一个字母条目。
List<String> nameList = Arrays.asList(mNames);Collections.sort(nameList, new Comparator<String>() {    @Override    public int compare(String o1, String o2) {        char letter = Character.toUpperCase(Pinyin.toPinyin(o1.charAt(0)).charAt(0));        char nextLetter = Character.toUpperCase(Pinyin.toPinyin(o2.charAt(0)).charAt(0));        if (letter == nextLetter) {            return 0;        } else {            return letter - nextLetter;        }    }});
遍历姓名列表,构造条目模型。怎么让多个同英文字母的姓名分组前插入一个字母条目呢?很简单,每次遍历时判断是不是和上一个字母一致,不一致才添加一个。记录字母条目的位置,其实就是添加字母条目时,获取自己在列表数据集的位置,因为每次都添加到尾部,所以直接取列表数据集的最后一位的位置即可。
//字母char letter = 0;for (int i = 0; i < nameList.size(); i++) {    if (i == 0) {        //获取文字第一个字母        String letterPinyin = Pinyin.toPinyin(nameList.get(i).charAt(0));        letter = Character.toUpperCase(letterPinyin.charAt(0));        mListItems.add(new ContactLetterModel(String.valueOf(letter)));    } else {        //如果下一个条目的首字母和上一个的不一样,则插入一条新的字母条目        String letterPinyin = Pinyin.toPinyin(nameList.get(i).charAt(0));        char nextLetter = Character.toUpperCase(letterPinyin.charAt(0));        if (nextLetter != letter) {            letter = nextLetter;            mListItems.add(new ContactLetterModel(String.valueOf(letter)));            //记录字母条目的位置,后续拉动字母选择条时跳转位置            mLetterPositionMap.put(String.valueOf(letter).toUpperCase(), mListItems.size() - 1);        }    }    //添加联系人条目    mListItems.add(new ContactModel(nameList.get(i)));}mListAdapter.notifyDataSetChanged();
完整代码
public class SlideBar extends View {    /**     * 默认选中时,文字颜色     */    private final int mDefaultSelectTextColor = Color.parseColor("#FFFFFF");    /**     * 默认未选中时,文字颜色     */    private final int mDefaultUnSelectTextColor = Color.parseColor("#202020");    /**     * 默认选中时,滑动条背景颜色     */    private final int mDefaultSelectBgColor = Color.parseColor("#66202020");    /**     * 默认未选中时,滑动条背景颜色     */    private final int mDefaultUnSelectBgColor = Color.parseColor("#00000000");    /**     * 索引条宽度     */    private int mWidth;    /**     * 字母表     */    private String[] mLetter = new String[]{"#", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};    /**     * 文字区域,保存为成员变量是为了复用     */    private Rect mTextRect;    /**     * 每个字母的高度     */    private int mCellHeight;    /**     * 是否拖动索引条中     */    private boolean mTouched = false;    /**     * 选择回调     */    private OnSelectItemListener mListener;    /**     * 画笔     */    private Paint mPaint;    private int mSelectTextColor;    private int mUnSelectTextColor;    private int mSelectBgColor;    private int mUnSelectBgColor;    public SlideBar(Context context) {        super(context);        init(context, null);    }    public SlideBar(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        init(context, attrs);    }    public SlideBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init(context, attrs);    }    private void init(Context context, @Nullable AttributeSet attrs) {        initAttrs(context, attrs);        initPaint();    }    private void initAttrs(Context context, @Nullable AttributeSet attrs) {        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SlideBar);        //选中时文字的颜色        mSelectTextColor = array.getColor(R.styleable.SlideBar_slb_select_txt_color, mDefaultSelectTextColor);        //未选中时文字的颜色        mUnSelectTextColor = array.getColor(R.styleable.SlideBar_slb_un_select_txt_color, mDefaultUnSelectTextColor);        //选中时,滑动条背景颜色        mSelectBgColor = array.getColor(R.styleable.SlideBar_slb_select_bg_color, mDefaultSelectBgColor);        //未选中时,滑动条背景颜色        mUnSelectBgColor = array.getColor(R.styleable.SlideBar_slb_un_select_bg_color, mDefaultUnSelectBgColor);        array.recycle();    }    private void initPaint() {        mPaint = new Paint();        mPaint.setTextSize(sp2px(getContext(), 11f));        if (mTouched) {            mPaint.setColor(mSelectTextColor);        } else {            mPaint.setColor(mUnSelectTextColor);        }        mPaint.setAntiAlias(true);        mPaint.setStyle(Paint.Style.STROKE);        mTextRect = new Rect();    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        mWidth = w;        //每行的高度,计算平均分每个字母占的高度        mCellHeight = h / mLetter.length;    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        setMeasuredDimension(measureWidth(widthMeasureSpec), heightMeasureSpec);    }    /**     * 测量本身的大小,这里只是测量宽度     *     * @param widthMeaSpec 传入父View的测量标准     * @return 测量的宽度     */    private int measureWidth(int widthMeaSpec) {        /*定义view的宽度*/        int width;        /*获取当前 View的测量模式*/        int mode = MeasureSpec.getMode(widthMeaSpec);        /*         * 获取当前View的测量值,这里得到的只是初步的值,         * 我们还需根据测量模式来确定我们期望的大小         * */        int size = MeasureSpec.getSize(widthMeaSpec);        /*         * 如果,模式为精确模式         * 当前View的宽度,就是我们的size         * */        if (mode == MeasureSpec.EXACTLY) {            width = size;        } else {            /*否则的话我们就需要结合padding的值来确定*/            int desire = size + getPaddingLeft() + getPaddingRight();            if (mode == MeasureSpec.AT_MOST) {                width = Math.min(desire, size);            } else {                width = desire;            }        }        return width;    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        //触摸时改变背景颜色        if (mTouched) {            canvas.drawColor(mSelectBgColor);            mPaint.setColor(mSelectTextColor);        } else {            canvas.drawColor(mUnSelectBgColor);            mPaint.setColor(mUnSelectTextColor);        }        for (int i = 0; i < mLetter.length; i++) {            String text = mLetter[i];            //测量文字宽高            mPaint.getTextBounds(text, 0, text.length(), mTextRect);            int textWidth = mTextRect.width();            int textHeight = mTextRect.height();            //文字一半的宽度            float textHalfWidth = textWidth / 2.0f;            //字母文字的起点X坐标,控件的宽度的一半再减去文字的一半            float x = (mWidth / 2.0f) - textHalfWidth;            //起点文字的Y坐标            float y = (mCellHeight / 2.0f + textHeight / 2.0f + mCellHeight * i);            //画文字            canvas.drawText(mLetter[i], x, y, mPaint);        }    }    @Override    public boolean onTouchEvent(MotionEvent event) {        float y = event.getY();        //计算当前触摸的字母的位置        int index = (int) (y / mCellHeight);        //触摸        if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {            mTouched = true;            if (index >= 0 && index < mLetter.length) {                if (mListener != null) {                    mListener.onItemSelect(index, mLetter[index]);                }            } else {                return super.onTouchEvent(event);            }        } else {            //松手            mTouched = false;            if (mListener != null) {                mListener.onItemUnSelect();            }        }        //触摸改变时,不断通知重绘来绘制索引条        invalidate();        return true;    }    public interface OnSelectItemListener {        /**         * 选择时回调         *         * @param position     选中的位置         * @param selectLetter 选中的字母         */        void onItemSelect(int position, String selectLetter);        /**         * 松手取消选中时回调         */        void onItemUnSelect();    }    public void setOnSelectItemListener(OnSelectItemListener listener) {        mListener = listener;    }    public static int dip2px(Context context, float dipValue) {        final float scale = context.getResources().getDisplayMetrics().density;        return (int) (dipValue * scale + 0.5f);    }    public static int px2dp(Context context, float pxValue) {        final float scale = context.getResources().getDisplayMetrics().density;        return (int) (pxValue / scale + 0.5f);    }    private int sp2px(Context context, float spVal) {        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,                spVal, context.getResources().getDisplayMetrics());    }}
到这里就结束啦. 往期精彩回顾:
  • Android实现短信验证码自动填充功能

  • Android仿echo精美弹幕功能

  • Android实现头像重叠排列功能

  • Android仿QQ个性标签功能

  • Android仿QQ侧滑删除的功能

b50c4ee749eba9cadccb4bae7d98b1a3.png

a8ec821ffc8a54db4c02e85282bd1988.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值