定制一个类似地址选择器的view

本篇文章已授权微信公众号码个蛋独家发布

前言:

这几天也是闲来无事,看看有什么和Scroller相关的控件需要巩固下,原因很简单,前几天看到相关的控件:不错的一个卷尺view,于是乎自己也不能光看别人的demo啊,所以自己也就撸了一个带有滑动的地址选择器的view了。

view的来源gif图:

标本地址选择器.gif

看到这的时候,我就大致有点思路了,所以自己的地址选择器view也是能登场了。

自己撸的view:

自己撸的地址选择器view.gif

由于这个地址的数据量太大了,我就随便弄了几个城市的数据。后续可以继续添加其他的数据。

使用:

布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <com.library.multiselct.MultiSelectView
        android:id="@+id/select_view"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@android:color/white" />

</LinearLayout>

对MultiSelectView选中内容的监听

MultiSelectView multiSelectView = (MultiSelectView) conentView.findViewById(R.id.select_view);
multiSelectView.setOnAllSelect(new MultiSelectView.OnAllSelect() {
    @Override
    public void select(String text) {
        //回调的处理
        ((MainActivity) context).setAddress(text);
    }
});

数据源的处理:

multiSelectView.validateList(Constant.initData());

讲解:

在讲解之前还是来一个整个view的布局情况草图:

MultiSelectView布局分布图.png

从这里不难发现外层是一个ViewGroup,里面是三个我们需要滑动处理的View了。

添加3个MultiSelectItem的view

private void initItem() {
    for (int i = 0; i < 3; i++) {
        MultiSelectItem multiSelectItem = new MultiSelectItem(getContext());
        //滑动的索引位置监听
        multiSelectItem.setScrollListener(this);
        addView(multiSelectItem);
    }
}

对3个MultiSelectItem的view测量

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = MeasureSpec.getSize(widthMeasureSpec);
    //这里子view的宽度按照父view的宽度平分
    int childWidth = (int) (width * 1.0f / getChildCount());
    for (int i = 0; i < getChildCount(); i++) {
        measureChild(getChildAt(i), MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
    }
}

对3个MultiSelectItem的view进行layout

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (int i = 0; i < getChildCount(); i++) {
        View item = getChildAt(i);
        //子view的横坐标起点根据(总宽度*i)/getChildCount())
        item.layout((int) (getWidth() * i * 1.0f / getChildCount()), 0, (int) (getWidth() * i * 1.0f / getChildCount()) + item.getMeasuredWidth(), item.getMeasuredHeight())
    }
}

MultiSelectView代码也太简单了点吧,没错,这就是Viewgroup三步曲代码。

对于父Viewgroup的三步曲代码已经搞定了,下面要进入到子View(MultiSelectItem)的代码中去看看了,首先完成下静态的分行处理,分行处理其实就是画行数-1条横线了。

画横线:

private void drawLine(Canvas canvas) {
    int lineCount = DEFRAULT_DISPLAY_COUNT - 1;
    for (int i = 0; i < lineCount; i++) {
        //每条横线的y轴起点是(总高度 * (i + 1) / DEFRAULT_DISPLAY_COUNT)
        canvas.drawLine(0, getHeight() * 1.0f * (i + 1) / DEFRAULT_DISPLAY_COUNT, getWidth(), getHeight() * 1.0f * (i + 1) / DEFRAULT_DISPLAY_COUNT, linePaint);
    }
}

绘制内容:

private void drawItem(Canvas canvas) {
    for (int i = 0; i < selectBeanList.size(); i++) {
        SelectBean selectBean = selectBeanList.get(i);
        String name = selectBean.name;
        if (offset + i * diffY >= height || (offset + i * diffY) + diffY <= 0) {
            continue;
        } else {
            //这个是缩放和透明度的代码,先不用看
            if (i == currentIndex) {
                textPaint.setTextSize(currentTextSize);
                textPaint.setAlpha((int) (255 * currentAlpha));
            } else {
                textPaint.setTextSize(otherTextSize);
                textPaint.setAlpha((int) (255 * otherAlpha));
            }
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            float allHeight = fontMetrics.descent - fontMetrics.ascent;
            canvas.drawText(name, width * 1.0f / 2, offset + i * diffY + diffY / 2 - allHeight / 2 - fontMetrics.ascent, textPaint);
        }
    }
}

上面的绘制代码中,有两个变量offsetdiffY,offset是当前view滑动到的位置,也即是我们第一个item的起点坐标,diffY是每一行需要的高度,可以看下他们的初始化的值。

offset和diffY初始化:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    height = h;
    width = w;
    //offset起点坐标也即是我们第二行的坐标
    offset = (float) (h * 1.0 / DEFRAULT_DISPLAY_COUNT);
    //每一个间隔的y轴距离是(总高度/DEFRAULT_DISPLAY_COUNT)
    diffY = (float) (h * 1.0 / DEFRAULT_DISPLAY_COUNT);
    //省略代码
}

知道了这两个变量后,咋们再来看下绘制内容的代码,首先在遍历数据源的时候,有越界的判断,分别是有四种情况是在绘制区域外的:
offset + i * diffY > height:item的上边缘在height之下
(offset + i * diffY) + diffY < 0:item的下边缘在0之上
(offset + i * diffY == height):item的上边缘在height位置
(offset + i * diffY) + diffY == 0:item的下边缘在0这个位置

这里给一个offset初始状态下(offset=h * 1.0 / DEFRAULT_DISPLAY_COUNT)的草图出来,这里只画一个MultiSelectItem的情况:

初始状态下数据源分布情况.png

滑动处理:

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (velocityTracker == null) {
        velocityTracker = VelocityTracker.obtain();
    }
    velocityTracker.addMovement(event);
    float y = event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mScroller.forceFinished(true);
            lastY = y;
            dy = 0;
            break;
        case MotionEvent.ACTION_MOVE:
            dy = y - lastY;
            //滑动过程中,改变值的过程
            validateValue();
            break;
        case MotionEvent.ACTION_UP:
            //抬起的时候对速度进行处理
            calculateVelocity();
            break;
    }
    lastY = y;
    return true;
}

滑动过程中对offset的处理:

private void validateValue() {
    offset += dy;
    if (offset <= maxOffset) {
        offset = maxOffset;
    }
    if (offset >= minOffset) {
        offset = minOffset;
    }
    scrollTochangeChilds();
    postInvalidate();
}

//滑动的位置到了需要改变childs数据的时候了
private void scrollTochangeChilds() {
    if (Math.abs(offset) % diffY <= maxDeviation) {
        if (offset > 0) {
            //如果offset在view的起点下面,计算的时候需要-diffY
            currentIndex = Math.round(Math.abs(offset - diffY) * 1.0f / diffY);
        } else {
            //如果offset在view的起点上面,计算的时候需要+diffY
            currentIndex = Math.round((Math.abs(offset) + diffY) * 1.0f / diffY);
        }
        //当前被选中的放大
        currentTextSize = maxTextSize;
        //当前被选中的alpha值最大
        currentAlpha = maxAlpha;
        //接口回调,给MultiSelectView刷新数据
        if (mScrollListener != null) {
            mScrollListener.end(this, currentIndex);
        }
    }
}

上面代码就是onMove的操作处理,其中上面有offset临界值处理:
maxOffset:滑动的最大的位置
minOffset:滑动的最小的位置
这两个值是哪来的呢:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    //省略代码
    minOffset = diffY;
    maxOffset = -diffY * (this.selectBeanList.size() - 2);
}

这里我画两张草图大家就知道这两个临界值是怎么回事了:

offset<= minOffset图解.png
offset>=maxOffset图解(图中数据源假如是6个).png

相信看图能知道是怎么回事了吧,临界值就是这么来的。
上面的move操作里面还进行了一个currentIndex的处理,当认为Math.abs(offset) % diffY <= maxDeviation的时候,则需要重新获取新的被选中的index了。

抬起过程中对offset的处理:

private void calculateVelocity() {
    velocityTracker.computeCurrentVelocity(1000);
    float yVelocity = velocityTracker.getYVelocity();
    //大于这个值才会被认为是fling
    if (Math.abs(yVelocity) > minFlingVelocity) {
        //如果是当前位置在maxOffset处了,并且继续往上滑动则不处理或者 当前位置在minOffset处了,并且继续往下滑动则不处理
        if ((offset == maxOffset && yVelocity < 0) || (offset == minOffset && yVelocity 
            return;
        }
        int startY = Math.round(offset);
        //结束位置通过速度来判断了
        endY = Math.round(yVelocity / 10) + startY;
        //结束位置也是需要进行限制的
        if (endY <= maxOffset) {
            endY = maxOffset;
        }
        if (endY >= minOffset) {
            endY = minOffset;
        }
        //和move的时候计算currentIndex是一样的
        if (endY > 0) {
            currentIndex = Math.round(Math.abs(endY - diffY) * 1.0f / diffY);
        } else {
            currentIndex = Math.round((Math.abs(endY) + diffY) * 1.0f / diffY);
        }
        //endY的位置是需要diffY成整数倍的,并且是与currentIndex成反比的
        endY = diffY - currentIndex * diffY;
        mScroller.startScroll(0, startY, 0, (int) (endY - startY));
        invalidate();
    } else {
        //如果滑动速度不是很大,不需要fling的
        releaseMoveTo();
    }
}
//松手的时候,移动到最近的一个index上
private void releaseMoveTo() {
    if (offset > 0) {
        currentIndex = Math.round(Math.abs(offset - diffY) * 1.0f / diffY);
    } else {
        currentIndex = Math.round((Math.abs(offset) + diffY) * 1.0f / diffY);
    }
    int startY = Math.round(offset);
    endY = diffY - currentIndex * diffY;
    mScroller.startScroll(0, startY, 0, (int) (endY - startY));
    invalidate();
}

@Override
public void computeScroll() {
    super.computeScroll();
    //返回true表示滑动还没有结束
    if (mScroller.computeScrollOffset()) {
        offset = mScroller.getCurrY();
        scrollTochangeChilds();
        postInvalidate();
    }
}

对于MultiSelectItem整个代码基本就是这些了,可能还就是一些数据源的初始化和变量的一些初始化没说了,重点都已经介绍完了。

剩下还有MultiSelectView中被选中时的数据回调了,这里我就直接贴代码了:

@Override
public void end(MultiSelectItem multiSelectItem, int index) {
    //如果是第1个MultiSelectItem中的某一个item被选中的话
    if (multiSelectItem == getChildAt(0)) {
        Log.d("MultiSelectView", "end:" + index);
        this.selectBeanList2 = this.selectBeanList1.get(index).childs;
        this.selectBeanList3 = this.selectBeanList2.get(0).childs;
        ((MultiSelectItem) getChildAt(1)).resetList(this.selectBeanList2);
        ((MultiSelectItem) getChildAt(2)).resetList(this.selectBeanList3);
        if (onAllSelect != null) {
            onAllSelect.select(this.selectBeanList1.get(index).name + " " + this.selectBeanList2.get(0).name + " " + this.selectBeanList3.get(0).name);
        }
    } else if (multiSelectItem == getChildAt(1)) {//如果是第2个MultiSelectItem中的某一个item被选中的话
        this.selectBeanList3 = this.selectBeanList2.get(index).childs;
        ((MultiSelectItem) getChildAt(2)).resetList(this.selectBeanList3);
        if (onAllSelect != null) {
            onAllSelect.select(this.selectBeanList1.get(((MultiSelectItem) getChildAt(0)).getCurrentIndex()).name + " " + this.selectBeanList2.get(index).name + " " + this.selectBeanList3.get(0).name);
        }
    } else {
        //如果是第3个MultiSelectItem中的某一个item被选中的话
        if (onAllSelect != null) {
            onAllSelect.select(this.selectBeanList1.get(((MultiSelectItem) getChildAt(0)).getCurrentIndex()).name + " " + this.selectBeanList2.get(((MultiSelectItem) getChildAt(1)).getCurrentIndex()).name + " " + this.selectBeanList3.get(index).name);
        }
    }
}
private OnAllSelect onAllSelect;
public void setOnAllSelect(OnAllSelect onAllSelect) {
    this.onAllSelect = onAllSelect;
}
//选中内容回调
public interface OnAllSelect {
    void select(String text);
}

总结:

  • MultiSelectView中添加3个MultiSelectItem
  • MultiSelectView中对3个MultiSelectItem进行测量
  • MultiSelectView中对3个MultiSelectItem进行layout
  • MultiSelectItem首先把静态的行分割线画出来
  • MultiSelectItemonTouch的处理,边界、索引等
  • MultiSelectView中完成被选中的item的内容回调

代码传送门

更多你喜欢的文章

仿360手机助手下载按钮
仿苹果版小黄车(ofo)app主页菜单效果
设计一个银行app的最大额度控件
带你实现ViewGroup规定行数、item居中的流式布局
定制一个类似地址选择器的view
3D版翻页公告效果
一分钟搞定触手app主页酷炫滑动切换效果
快速利用RecyclerView的LayoutManager搭建流式布局
用贝塞尔曲线自己写的一个电量显示的控件
快速搞定一个自定义的日历
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容