Android自定义View——一个可定制的六边形阵列

关于六边形的自定义View网上已经有很多了,但目前来看都是固化的UI,可定制性不高,所以我这里将六边形与坐标绑定,这样的话我们就可以随意组合六边形形成我们需要的一个图案。
基本思路也很简单,一句话——明确行的标准。因为六边形组合起来的行是不规整的,它的列是规整的。那坐标由谁来控制呢?这里我选择父级容器,也就是说我们为这个自定义的View再自定义一个ViewGroup。X,Y坐标是view的自定义属性,viewgroup读出坐标数据进行测量与布局。

    <declare-styleable name="HexagonView">
        <attr name="x" format="integer"/>
        <attr name="y" format="integer"/>
        <attr name="viewColor" format="color"/>
        <attr name="hvtext" format="string"/>
        <attr name="hvtextSize" format="dimension"/>
        <attr name="hvtextColor" format="color"/>
    </declare-styleable>

六边形的话很好画,简单的正切关系,然后利用path做路径。

 float cx=getWidth()/2;
        float cy=getHeight()/2;
        float length=cx-PADDING;
        float a=(float) Math.sqrt(3)*length/2;  //邻边长度
        mPaint.setColor(mColor);
        mPaint.setAlpha(mAlpha);
        mPath.moveTo(PADDING,cy);
        //画一个正六边形
        mPath.lineTo(PADDING+length/2f,cy-a);
        mPath.lineTo(3/2f*length+PADDING,cy-a);
        mPath.lineTo(cx+length,cy);
        mPath.lineTo(3/2f*length+PADDING,cy+a);
        mPath.lineTo(PADDING+length/2f,cy+a);
        mPath.lineTo(PADDING,cy);
        canvas.drawPath(mPath,mPaint);
        if(mText!=null){
            mPaint.setTextAlign(Paint.Align.CENTER);
            mPaint.setTextSize(DisplayUtil.sp2px(mContext,mTextSize));
            mPaint.setColor(mTextColor);
            //文字居中显示
            Paint.FontMetricsInt fontMetricsInt=mPaint.getFontMetricsInt();
            float baseline=cy+(fontMetricsInt.descent-fontMetricsInt.ascent)/2-fontMetricsInt.descent;
            canvas.drawText(mText,cx,baseline,mPaint);
        }

然后我们再做一个简单的点击效果,我这里选择将透明度减半,效果还是挺明显的

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action=event.getAction();
        Point point=new Point((int)event.getX(),(int)event.getY());
        switch (action){
            case MotionEvent.ACTION_DOWN:{
                if(isInHexagon(mPath,point)){
                    mAlpha=150;
                    listener.onClick(getMX(),getMY());
                    invalidate();
                }
                break;
            }
            case MotionEvent.ACTION_UP:{
                mAlpha=255;
                invalidate();
                break;
            }
            default:
                break;
        }
        return true;
    }

这里还涉及到一个不规则(也可以说是非矩形)图形的边界判定问题,之前我也不太清楚,这里我们应该使用Region的setPath与contains这两个API

    /**
     * 判断点是否在边界内
     * @param path
     * @param point
     * @return
     */
    private boolean isInHexagon(Path path,Point point){
        RectF bounds=new RectF();
        path.computeBounds(bounds,true);
        Region region=new Region();
        region.setPath(path,new Region((int)bounds.left,(int)bounds.top,
                (int)bounds.right,(int)bounds.bottom));
        return region.contains(point.x,point.y);
    }

最后给外部提供一个接口

    public void setOnClickHVListener(onClickHVListener listener){
        this.listener=listener;
    }
    public interface onClickHVListener{
        void onClick(int x,int y);
    }

到这里我们的自定义View就完成了,然后就是自定义的一个Viewgroup。与自定义View一样需要考虑测量时的自适应问题。对于我们这个六边形阵列来说,我们很清楚,最大的X坐标将控制容器的宽度,最大的Y坐标将控制容器的高度(其实在高度的问题上还有一种特殊情况是因为行的不规整造成的,也就是说最大Y坐标可能有两个高度,我们只能选最高的那个)

    private int measureSize(int measureSpec,boolean isWidth){
        int measureSize;
        int mode= View.MeasureSpec.getMode(measureSpec);
        int size=View.MeasureSpec.getSize(measureSpec);
        if(mode==MeasureSpec.EXACTLY)
            measureSize=size;
        else {
            measureSize = 0;
            int count=getChildCount();
            HexagonView view;
            view=(HexagonView)getChildAt(0);
            int maxX=view.getMX(),maxY=view.getMY();
            if(count==1){
                //当只有一个子View的时候强制设定为子View的大小
                measureSize=(isWidth?mChildWidth:mChildHeight);
            }else if(count>0) {
                //遍历获得最大X、Y坐标
                for (int i = 1; i < count; i++) {
                    view=(HexagonView)getChildAt(i);
                    int mX=view.getMX(),mY=view.getMY();
                    if(mX>maxX)
                        maxX=mX;
                    if(mY>=maxY) {
                        maxY = mY;
                    }
                }
                boolean temp=false; //获取最大Y坐标并不能表示高度为Y个子View
                //判断最大Y坐标的一行是否有奇数的X坐标(突出一半的六边形)
                for (int i = 1; i < count; i++) {
                    view = (HexagonView) getChildAt(i);
                    int mX = view.getMX(), mY = view.getMY();
                    if(mY==maxY&&mX%2==1)
                        temp=true;
                }
                int line = mChildWidth / 2;
                //宽度的计算分X坐标为奇数和偶数两种情况
                if(isWidth){
                    if(maxX%2==0)
                        measureSize = (maxX / 2 + 1) * mChildWidth + line * maxX / 2;
                    else
                        measureSize=(maxX+1)/2*mChildWidth+(maxX-1)/2*line+(mChildWidth-line/2);
                }else{//高度的计算同样分两种情况(是否突出一半的六边形)
                    if(temp)
                        measureSize=mChildHeight*(maxY+1)-maxY*2*(mChildHeight/2-(int)(line/2*Math.sqrt(3)))
                                +(int)(line/2*Math.sqrt(3));
                    else
                        measureSize=mChildHeight*(maxY+1)-maxY*2*(mChildHeight/2-(int)(line/2*Math.sqrt(3)));
                }
            }
        }
        return measureSize;
    }

然后是layout方法,因为这个阵列的行不规则,列是规则的,所以我们只要搞定了行,列就好说了(垂直平移嘛),所以我们选择从每一行入手。

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int count=getChildCount();
        HexagonView view;
        for(int i=0;i<count;i++){
            view=(HexagonView)getChildAt(i);
            int mX=view.getMX(),mY=view.getMY();
            int l,t,r,b;
            int line = mChildWidth / 2;
            //整体分成X坐标是否为偶数两种情况,然后在Y坐标为0的基础上进行向下的平移
            if (mX % 2 == 0) {
                l = (mX / 2 + 1) * mChildWidth + line * mX / 2 - mChildWidth;
                if(mY==0) {
                    t = (mY / 2) * mChildHeight;
                }else{
                    t = mY*(int)(Math.sqrt(3)*line);
                }
            }else{
                l=(mX+1)/2*mChildWidth+(mX-1)/2*line-line/2;
                if(mY==0){
                    t=(int)(1/2f*(mY*mChildHeight+Math.sqrt(3)*line));
                }else{
                    t =(int)((mY+1/2f)*(Math.sqrt(3)*line));
                }
            }
            r=l+mChildWidth;
            b=t+mChildHeight;
            getChildAt(i).layout(l, t, r, b);
        }
    }

然后我们将之前view的接口在viewgroup里面实现,目的是让Activity通过viewgroup可以操作所有的view

HexagonsLayout extends ViewGroup implements HexagonView.onClickHVListener
···
···
    private onClickItemListener listener;
    public void setOnClickItemListener(onClickItemListener listener){
        this.listener=listener;
    }
    public interface onClickItemListener{
        void onClickItem(int x,int y);
    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int count=getChildCount();
        HexagonView hexagonView;
        for(int i=0;i<count;i++){
            hexagonView=(HexagonView) getChildAt(i);
            hexagonView.setOnClickHVListener(this);
        }
    }
    @Override
    public void onClick(int x, int y) {
        listener.onClickItem(x,y);
    }

最后我们在AS里看下viewgroup的自适应效果


image.png

还有点击事件的效果


未命名.gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342