关于六边形的自定义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的自适应效果
还有点击事件的效果