前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >联系人字母导航集大成者

联系人字母导航集大成者

原创
作者头像
jerrypxiao
修改2019-08-09 17:22:24
1.5K0
修改2019-08-09 17:22:24
举报
文章被收录于专栏:音视频专栏音视频专栏

1. 背景说明

联系人字母导航条已经出来很多年都交互了,其UI组合无非是悬浮字母列表+侧边栏都字母选择(PinnedHeadList+siderBar)。这里炒一下现饭,做了如下几个方面的事情。

  • 实现基本的字母导航控件;
  • 总结一下现有的开源导航控件的实现方案
  • 最后,实现一个有意思的动画效果。

最终效果
最终效果

2. 实现原理分析

2.1 基本问题

这里右侧的导航条可能放到一个高度有限的界面,这样导航条的高度可能很矮。

这样字母占用空间的大小就需要提前设置好或者动态计算。

(1)提前设置好,Android的机型适配又是一个不小的工作;

一般从需求出发,界面上字体的大小需要提前设计好,不然用户也看不清,也不好操作,你乱设置一个自己认为靠谱的界面设计师也不答应。

(2)动态计算,这里需要有个靠谱的计算方法,而且不影响性能;

这里Android O中出了一个Autosizing特性,可以参考其使用:文字太多?控件太小?试试 TextView 的新特性 Autosizing 吧!

android O Autosizing
android O Autosizing

深入了解一下源码,实际上就是用二分法试出大小然后动态设置上去。参考,TextView 的新特性,Autosizing 到底是如何实现的? | 源码分析

这里,建议如果界面是全屏幕的导航化,直接使用第一种方案,效率会高一点,没有必要做这么多复杂的操作;这里,我们实现两套方案对比一下。

图1 导航绘制图
图1 导航绘制图

如图看出,字母导航条实际上就是一个自定义View的实现。要画的好看,主要要解决的问题如下:

  • 画字母

计算整个字母导航条的高度;

计算每个字母的位置

合理设置字母的字号大小

  • 处理触摸事件

计算触摸位置

设置回调

  • 画放大的字母效果

计算绘制高宽

计算放大的字母位置

2.2 额外的需求

导航条中可能设置非字符,可能是图标,这就需要额外的绘制图片,我们循序渐进,先使用绘制字母的,后面逐步扩展。

3. 具体实现

根据上面的分析,分步骤实现对应的部分。

3.1 画字母

3.1.1 静态设置字母的大小

首先,要确定View的大小,一些开源的实现是将View直接布满整个屏幕(match_parent属性),然后在右侧进行绘制,事件处理在字母区域返回true,其他地方返回false,感觉没有必要,这里还是将wrap_content属性利用起来。

重写View的onMeasure方法,先看下super的默认实现

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

如果自定义View没有重写onMeasure函数,MeasureSpec.AT_MOST跟MeasureSpec.AT_EXACTLY

的表现是一样的。也就是wrap_content就跟match_parent一个效果,占满全屏幕。

这里根据属性来设置字体的大小,后面讨论动态改变的方式。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    Rect rectBound = new Rect();
    mPaint.getTextBounds("W",0,1,rectBound);
    int w = rectBound.width() + (int)padding;
    int h = rectBound.height() + (int)padding;

    int defaultWidth = getPaddingLeft() + w + getPaddingRight();
    int lettersHeight = ((mLetters==null) ? 0 : h * mLetters.length);
    int defaultHeight = getPaddingTop() + lettersHeight + getPaddingBottom();
    int width = measureHandler(widthMeasureSpec,defaultWidth);
    int height = measureHandler(heightMeasureSpec,defaultHeight);

    setMeasuredDimension(width,height);
}

private int measureHandler(int measureSpec,int defaultSize){

    int result = defaultSize;
    int measureMode = MeasureSpec.getMode(measureSpec);
    int measureSize = MeasureSpec.getSize(measureSpec);
    if(measureMode == MeasureSpec.EXACTLY){
        result = measureSize;
    }else if(measureMode == MeasureSpec.AT_MOST){
        result = Math.min(defaultSize,measureSize);
    }
    return result;
}

其中,mPaint为Paint对象初始化view的时候通过属性设置了字号的大小,将字母“W”作为参考字母,计算他的宽和高度。加一个padding的属性,这样得到一个默认的高度(defaultHeight)和宽度(defaultWidth)。

默认高宽
默认高宽

然后MeasureSpec得到父亲控件的高和宽,根据类型,EXACTLY(match_parent或者精确值),AT_MOST

(wrap_content)设置为View正真的高和宽。

然后就可以画出字母,其中mHeight为实际View高度,在onLayout中(mHeight = getHeight())

private void drawLetters(Canvas canvas){
    if(mLetters == null){
        return;
    }
    //字母的个数
    int len = mLetters.length;
    //单个字母的高度
    int singleHeight = mHeight/len;
    for (int i = 0; i < len; i++) {
        //计算位置
        Paint tempPaint = null;
        if(i == mChoose && isTouch){
            tempPaint = mSelectedPaint;
        }else{
            tempPaint = mPaint;
        }
        //要画的字母的x,y坐标
        float x = mWidth / 2;
        float y = singleHeight*(i+1)- tempPaint.measureText(mLetters[i])/2;
        //画字母
        canvas.drawText(mLetters[i], x, y, tempPaint);
    }
}

3.1.2 动态设置字母的大小

3.2 触摸事件处理

确定点击的位置是否在字母导航条的上面,(int)(y/mHeight * mLetters.length)即可得到选中字母的位置。其中,y为action的坐标。

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

    final int oldChoose = mChoose;
    //当前选中字母的索引
    final int newChoose = (int)(y/mHeight * mLetters.length);
    switch (action) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            isTouch = false;
            mChoose = -1;
            if (listener != null) {
                listener.onLetterTouching(false);
            }
            invalidate();
            return true;
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_MOVE:
            if (oldChoose != newChoose) {
                if (mLetters != null
                        && newChoose >= 0 && newChoose < mLetters.length) {
                    mChoose = newChoose;
                    if (listener != null) {
                        //计算位置
                        Rect rect = new Rect();
                        mPaint.getTextBounds(mLetters[mChoose], 0, mLetters[mChoose].length(), rect);
                        //字母的个数
                        int len = mLetters.length;
                        //单个字母的高度
                        int singleHeight = mHeight / len;
                        float yPos = singleHeight * (mChoose + 1) - mPaint.measureText(mLetters[mChoose]) / 2;
                        listener.onLetterChanged(mLetters[newChoose], action, yPos);
                    }
                }
                invalidate();
            }

            if (event.getAction() == MotionEvent.ACTION_DOWN) {//按下调用 onLetterDownListener
                isTouch = true;
                if (listener != null) {
                    listener.onLetterTouching(true);
                }
            }
            return true;
        default:
            return false;
    }
}

3.3 选中放大效果

选中效果可以简单到画出来,当然这样效率比较高。设计师一般要给你切一个他喜欢到图,没办法,绘制图上去吧。如下图,添加放大选图效果,这样整体高度需要加上图片的高度,字母起始的位置也要变化。

字母导航设计
字母导航设计
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    Rect rectBound = new Rect();
    mPaint.getTextBounds("W",0,1,rectBound);
    letterWidth = rectBound.width() + (int)padding;
    letterHeight = rectBound.height() + (int)padding;

    int defaultWidth = getPaddingLeft() + letterWidth + getPaddingRight() + mTipsWidth;
    int lettersHeight = ((mLetters==null) ? 0 : letterHeight * mLetters.length);
    int defaultHeight = getPaddingTop() + lettersHeight + getPaddingBottom() + mTipsHeight;
    int width = measureHandler(widthMeasureSpec,defaultWidth);
    int height = measureHandler(heightMeasureSpec,defaultHeight);

    setMeasuredDimension(width,height);
}

于是,这里默认的高和宽加上图片的高度和宽度。这样背景的显示无论如何都放得进去。startY要向下偏移半个字母都高度

起始位置
起始位置

绘制提示字母到背景上,这个图片不是标准到左右对称,所以中心要偏左一点

//draw tips letter
Rect rect = new Rect();
mTipsTextPaint.getTextBounds(mLetters[mChoose], 0, mLetters[mChoose].length(), rect);
float startTipsX = (int)(mTipsWidth* 4.0 / 7.0f - rect.width()/2.0);
float startTipsY = (int) (startTop + mTipsHeight / 2.0f + rect.height()/2.0);
canvas.drawText(mLetters[mChoose], startTipsX, startTipsY, mTipsTextPaint);

private boolean isValid(float x, float y){
    if(x >= mTipsWidth && x <= mWidth
            && y > mTipsHeight/2f && (y < mHeight - mTipsHeight/2f)){
        return true;
    }else {
        return false;
    }
}
case MotionEvent.ACTION_DOWN:
    boolean isValid = isValid(x, y);
    if(!isValid){
        return super.onTouchEvent(event);
    }

这里触摸事件加上位置都判断,防止误触;

以上就是字母导航基本的实现的几个方面。

4. 开源的一些实现分析

我们看下开源的实现,基本上都逃不过上面几个方面,我们稍微分析一下:

(1)https://github.com/zaaach/CityPicker start(2446 )

https://github.com/zaaach/CityPicker/blob/master/citypicker/src/main/java/com/zaaach/citypicker/view/SideIndexBar.java
https://github.com/zaaach/CityPicker/blob/master/citypicker/src/main/java/com/zaaach/citypicker/view/SideIndexBar.java
  • 按照字母个数均分屏幕高度,计算topMargin的计算代码很奇怪。
  • 没有画放大效果,而是在外面的view上设置
  • 代码一般,星星多可能是因为pinHead效果

(2)https://github.com/saiwu-bigkoo/Android-QuickSideBar start(601 )

https://github.com/saiwu-bigkoo/Android-QuickSideBar/blob/master/quicksidebar/src/main/java/com/bigkoo/quicksidebar/QuickSideBarView.java
https://github.com/saiwu-bigkoo/Android-QuickSideBar/blob/master/quicksidebar/src/main/java/com/bigkoo/quicksidebar/QuickSideBarView.java
  • 右侧垂直居中绘制字母,字母大小由属性设置
  • 放大效果的提示由额外的布局实现,这里用path绘制了一个特殊的圆角矩形(addRoundRect(RectF rect, float[] radii, Direction dir))
  • tipsView用addView的方式,绘制效率没那么高,右侧字母设置和位置摆放比较固定

(3)https://github.com/gjiazhe/WaveSideBar (start 1126)

https://github.com/gjiazhe/WaveSideBar/blob/master/wavesidebar/src/main/java/com/gjiazhe/wavesidebar/WaveSideBar.java
https://github.com/gjiazhe/WaveSideBar/blob/master/wavesidebar/src/main/java/com/gjiazhe/wavesidebar/WaveSideBar.java
  • 字母垂直居中显示,字母大小为属性设置,默认14sp,根据字体大小计算每个字母高度;
  • 凸显放大效果,根据位置计算上下各4个,计算偏移位置和缩放,根据点击位置然后绘制;
  • 整体代码还比较工整。

(4)https://github.com/Solartisan/WaveSideBar (start 1063)

https://github.com/Solartisan/WaveSideBar/blob/master/wave/src/main/java/cc/solart/wave/WaveSideBarView.java
https://github.com/Solartisan/WaveSideBar/blob/master/wave/src/main/java/cc/solart/wave/WaveSideBarView.java
  • 字母整体高度为右侧垂直居中,通过属性获取字体大小计算高度,可以设置Padding
  • drawRoundRect画了一个字母的背景,感觉好看
  • 选中的波浪用了三段二次贝塞尔曲线(Path类的 quadTo方法),通过控制贝塞尔控制点的x坐标的宽度来展示动画效果,用Path绘制放大的小球
  • 加入动画效果ValueAnimator
  • 代码还是比较工整

这些开源实现,都没有考虑到在有限的高度内,自适应字母的高度;当然一般字母导航的界面需要在全屏幕来展示,在半个屏幕来展示全屏的情况很少,比如我就遇到了。参考一下源码的二分法

private boolean updateTextSize(int heightMeasureSize, int needSize){
    if(mLetters == null){
        return false;
    }
    if(heightMeasureSize >= needSize){
        return false;
    }

    float fitTextSize = findLargestTextSizeWhichFits(heightMeasureSize, needSize);
    mPaint.setTextSize(fitTextSize);
    return true;
}

private float findLargestTextSizeWhichFits(int heightMeasureSize, int needSize){
    float curSize = mPaint.getTextSize();
    float minSize = 1;
    float maxSize = 100;

    int measuredHeight = heightMeasureSize - (getPaddingTop() + getPaddingBottom());
    while (minSize < maxSize) {
        curSize = (minSize + maxSize) / 2;
        if (calHeightOnMeasure(curSize) <= measuredHeight) {
            minSize = curSize + 1;
        } else {
            maxSize = curSize - 1;
        }
    }
    return curSize;
}

private int calHeightOnMeasure(float textSize) {
    int result = 0;
    int indexSize = mLetters.length;
    final Paint textPaint = mPaint;
    textPaint.setTextSize(textSize);
    textPaint.setAntiAlias(true);
    Rect bound = new Rect();
    for (int i = 0; i < indexSize; i++) {
        textPaint.getTextBounds(mLetters[i], 0, mLetters[i].length(), bound);
        int height = (bound.bottom - bound.top);
        result += height;
        result += (int)padding;
    }
    result = result + mTipsHeight;
    return result;
}

修改后看下代码,确实自适应了,当然这里源码的功能是在一个预设值列表里面进行二分,性能要好点,功能更强大点,后面有需要可以自己加上,这里只是实现最小功能,取个最大和最小值1和100

修改indexBar高度为400dp
修改indexBar高度为400dp

具体见源代码:https://github.com/xiaopengs/IndexBar

5. 加个自己的动画

加个流行的粘性贝塞尔动画

动画示意图
动画示意图
最终效果
最终效果

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 背景说明
  • 2. 实现原理分析
    • 2.1 基本问题
      • 2.2 额外的需求
      • 3. 具体实现
        • 3.1 画字母
          • 3.1.1 静态设置字母的大小
          • 3.1.2 动态设置字母的大小
        • 3.2 触摸事件处理
          • 3.3 选中放大效果
          • 4. 开源的一些实现分析
          • 5. 加个自己的动画
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档