美文网首页
Android自定义View——一个可定制的六边形阵列

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

作者: 奥利奥真好吃 | 来源:发表于2018-01-24 21:15 被阅读0次

    关于六边形的自定义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

    相关文章

      网友评论

          本文标题:Android自定义View——一个可定制的六边形阵列

          本文链接:https://www.haomeiwen.com/subject/accpaxtx.html