第一次写博客,写的不好,知识点不到位的请大神,大牛指点。不废话了直接进入主题。
先看下效果图:
大致说下我的思路:
一:对题目的思考。
二:继承View 主要重写 onDraw(Canvas),onMeasure(int,int),oTouchEvent(MotionEvent);三个方法;
三:回调。
我们来说明下;
九宫格是个控件,是个单一的控件,不是一组控件,所以我直接继承view ,而不继承ViewGroup。
要自定义控件必须要重写测量方法 onMeasure(int,int).
是不是这样得到控件width,height很简单,其实onMeasure这样写是有局限性的,你的控件必须要设定大小和fill_parent才可以这样使用。
如果要是wrap_content,就不可以这样用了,
<pre name="code" class="java">/**
* 作用是返回一个默认的值,如果MeasureSpec没有强制限制的话则使用提供的大小.否则在允许范围内可任意指定大小
* 第一个参数size为提供的默认大小,第二个参数为测量的大小
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// Mode = UNSPECIFIED,AT_MOST时使用提供的默认大小
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
// Mode = EXACTLY时使用测量的大小
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
这是在网上找的,可以看到当模式为MeasureSpec.UNSPECIFIED时,即未指明大小或warp_content,测量的结果为size;
而size为多少呢?
/**
* 这个方法需要被重写,应该由子类去决定测量的宽高值,
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
也就是这个size=getSuggestedMinimuWidth()和size=getSuggestedMinimuHeight()。
这两方法是View的两保护方法,下面是源码
*/
7285 protected int getSuggestedMinimumWidth() {
7286 int suggestedMinWidth = mMinWidth;
7287
7288 if (mBGDrawable != null) {
7289 final int bgMinWidth = mBGDrawable.getMinimumWidth();
7290 if (suggestedMinWidth < bgMinWidth) {
7291 suggestedMinWidth = bgMinWidth;
7292 }
7293 }
7294
7295 return suggestedMinWidth;
7296 }
7297
可以看到是直接返回最小的宽高。
我们在来看两段源码:
4162 public final int getMeasuredWidth() { 4163 return mMeasuredWidth; 4164 }
7191 protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { 7192 mMeasuredWidth = measuredWidth; 7193 mMeasuredHeight = measuredHeight; 7194 7195 mPrivateFlags |= MEASURED_DIMENSION_SET; 7196 } 7197
是不是发现什么。getMeasuredWidth()和getMeasuredHeight()两方法得到的值就是getDefaultSize()的返回值。
所以这两个方法与模式没关系。所以在自定义测量的时候可以直接用这两个方法。
不懂在上网查查,onMeasure(int ,int)先到这里。
再来说说onDraw(Canvas)
这个ondraw方法在view初始化后调用绘制画布。每当控件增加新的图案都会调用ondraw(canvas)方法,调用这个方法是重新绘制,而不是只绘制新增的部分。要在UI线程调用
invalidate()刷新画布才可以显示。Canvas是画图,创建Canvas有两种方法。一个是当做onDraw参数传进去,
另一中是: Bitmap b=Bitmap.createBitmap(200,200,Bitmap.Config.ARGB_888);
Canvas c=new Canvas(b);创建一个200*200的Bitmap当做Canvas操作画布。
不懂的可以上网查查。
我在细说一下我的代码:
最后一个参数为接口,
<pre name="code" class="java">public class CircleCanvas extends ListView { private Context mct; private Paint mpaint; //画笔 private Paint hollowPaint; private Paint linePaint; private int width; //view 宽,高 private int height; private int startRangeWidth; //起点坐标(x,y) private int startRangeHeight; private int endRangeWidth; //终点坐标(X,Y)两个坐标构成滑动区域。 private int endRangeHeight; private int spotIntervalWidth; //每个点之间的间隔 private int spotIntervalHeight; private List<SpotXY> spot; //存放9个点的坐标 private List<SpotXY> delSpot; //作用方面显示画的结果 private List<SpotXY> storeSpot; //储存选中的空心圆的坐标 即spot private List<Segment> segment; //储存画出的线段 private int i=0; private float r=100.0f; //大圆半径 private SpotXY xy; //一个点 private SpotXY xyTwo; private int tclWay=0; private boolean isspot=true; private static int countId=1; //每个点的id编号 private String pwd; //密码 private OnCircleCanvasOver oncirclecanvasover;
这个参数在储存画的线段的时候会用的,判断不要重复储存一样的线段
private static int countId=1; //每个点的id编号
初始化操作,
public CircleCanvas(Context context, AttributeSet attrs) { super(context, attrs); this.mct=context; spot=new ArrayList<SpotXY>(); delSpot=new ArrayList<SpotXY>(); storeSpot=new ArrayList<SpotXY>(); segment=new ArrayList<Segment>(); WindowManager wm=(WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics metric = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(metric); intn(); } public CircleCanvas(Context context) { super(context); this.mct=context; spot=new ArrayList<SpotXY>(); delSpot=new ArrayList<SpotXY>(); storeSpot=new ArrayList<SpotXY>(); segment=new ArrayList<Segment>(); WindowManager wm=(WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics metric = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(metric); intn(); } private void intn() { mpaint=new Paint(); mpaint.setColor(Color.YELLOW ); mpaint.setStrokeWidth(3); hollowPaint=new Paint(); hollowPaint.setStyle(Paint.Style.STROKE); hollowPaint.setStrokeWidth(20); hollowPaint.setColor(Color.BLUE ); linePaint=new Paint(); linePaint.setColor(Color.GREEN ); linePaint.setStrokeWidth(20); }
绘制,主要是这两个storeSpot为自己滑动过点的坐标集合,xy类型是SpotXY类这个是封装点的工具类。
有三参数x坐标 ,y坐标和点的id。上面表达式表示提出最近储存的点与即将要滑动的点画出线段。
protected void onDraw(Canvas canvas) { switch(tclWay) { case 0: for(int i=0;i<spot.size();i++) { canvas.drawCircle(spot.get(i).getSpotx(),spot.get(i).getSpoty(), 30, mpaint); } break; case 1: for(int i=0;i<spot.size();i++) { canvas.drawCircle(spot.get(i).getSpotx(),spot.get(i).getSpoty(), 30, mpaint); } canvas.drawCircle(xy.getSpotx(), xy.getSpoty(), r, hollowPaint); delSpot(xy); break; case 2: for(int i=0;i<spot.size();i++) { canvas.drawCircle(spot.get(i).getSpotx(),spot.get(i).getSpoty(), 30, mpaint); } for(int i=0;i<storeSpot.size();i++) { canvas.drawCircle(storeSpot.get(i).getSpotx(), storeSpot.get(i).getSpoty(), r, hollowPaint); } for(int i=0;i<segment.size();i++) { canvas.drawLine(segment.get(i).getStartX(), segment.get(i).getStartY(), segment.get(i).getEndX(), segment.get(i).getEndY(), linePaint); } xy=storeSpot.get(storeSpot.size()-1); delSpot(xy); } super.onDraw(canvas); }
移除已近划过的点delSpot(xy)
/** * 移除被选中的点 * @param xy2 */ private void delSpot(SpotXY xy2) { for(int i=0;i<delSpot.size();i++) { if(xy.getSpotx()==delSpot.get(i).getSpotx()&&xy.getSpoty()==delSpot.get(i).getSpoty()) { delSpot.remove(i); break; } } }
我是把手机分为4个区域,取中间相邻的两区域为手势可滑动区域。进而可以算出可以滑动区域的坐标,大小。
/** * 计算有效滑动区域 * @param width2 view 宽 * @param height2 view 高 * @param i 区域划分的个数 */ private void glidingArea(int width2, int height2, int i) { startRangeWidth=0; startRangeHeight=height2/i; endRangeWidth=width2; endRangeHeight=height2*3/i; spotIntervalWidth=width2/6; spotIntervalHeight=Math.abs((endRangeHeight-startRangeHeight)/6); //countId=0; XYZ(startRangeWidth,startRangeHeight,endRangeWidth,endRangeHeight,spotIntervalWidth,spotIntervalHeight); for(SpotXY sxy:spot){ delSpot.add(sxy); } }
countId是静态变量,在项目里不推崇大家用静态变量,这个id是在项目快要完成时加上的,想了一会只能用静态变量。
<pre name="code" class="java"><pre name="code" class="java"> /** * 计算9个点的坐标 * @param startRangeWidth2 * @param startRangeHeight2 * @param endRangeWidth2 * @param endRangeHeight2 * @param spotIntervalWidth2 * @param spotIntervalHeight2 * @return */ private void XYZ(int startRangeWidth2, int startRangeHeight2, int endRangeWidth2, int endRangeHeight2, int spotIntervalWidth2, int spotIntervalHeight2) { for(int i=0;i<3;i++) { for(int j=0;j<3;j++) { spot.add(new SpotXY((int) (spotIntervalWidth2*(2*(j+1)-1)), (int) (startRangeHeight2+spotIntervalHeight2*(2*(i+1)-1)),countId++)); } } //return spot; }
这是判断点击点 和滑动的时候在不在可滑动区域,要是在返回最近点的坐标。
/** * 返回离点击点最近的点 * @param widthX * @param widthY * @param spot2 * @return */ private SpotXY latelyClick(float widthX, float widthY, List<SpotXY> spot2) { if(widthY<endRangeHeight&&widthY>startRangeHeight) { for(SpotXY spotxy:spot2) { if(Math.abs(spotxy.getSpotx()-widthX)<r&&Math.abs(spotxy.getSpoty()-widthY)<r) { return new SpotXY(spotxy.getSpotx(),spotxy.getSpoty(),spotxy.getId()); } } } return null; }
下面事件处理份三处贴
记录点击点 并判断在不在可滑动区域内,在的画在最近的点加入storeSpot集合里(已经划过的点)。tclWay=1执行onDraw里的case 1; 刷新显示空心圆。
public boolean onTouchEvent(MotionEvent event) { switch(event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("X变化为:"+event.getX()); //invalidate(); float widthX=event.getX(); float widthY=event.getY(); xy=latelyClick(widthX,widthY,spot); if(xy!=null) { storeSpot.add(xy); tclWay=1; invalidate(); }
滑动时的事件处理,滑动的时候每时每刻都在判断是不是处在可滑动区域内,是否有新的点可以返回。如果有新点,还要遍历storeSpot集合看看里面有没有这个带没有就加入里面。然后在在这个点和上个点做为线段的两点加入到segment集合里。segment这个集合类型是Segment有四个参数即两个坐标点。tclWay变成2执行 onDraw画线段。
case MotionEvent.ACTION_MOVE: System.out.println("X变化为:"+event.getX()); float movewidthX=event.getX(); float movewidthY=event.getY(); xyTwo=latelyClick(movewidthX,movewidthY,spot); if(storeSpot.size()==0) //当点击不在规定范围时, 滑动到规定范围里执行次判断 { xy=xyTwo; } if(xyTwo!=null&&(xy.getSpotx()!=xyTwo.getSpotx()||xy.getSpoty()!=xyTwo.getSpoty())) { for(SpotXY sxy:storeSpot) { if(sxy.getSpotx()==xyTwo.getSpotx()&&sxy.getSpoty()==xyTwo.getSpoty()) { //storeSpot.add(xyTwo); isspot=false; break; }else{ isspot=true; } } if(isspot) { if(storeSpot.size()>0) { segment.add(new Segment(xy.getSpotx(), xy.getSpoty(), xyTwo.getSpotx(), xyTwo.getSpoty())); }else{ xy=xyTwo; } storeSpot.add(xyTwo); } tclWay=2; invalidate(); } break;
回到以前,回到只有九个点的状态
case MotionEvent.ACTION_UP: System.out.println("X变化为:"+event.getX()); tclWay=0; setPwd(); storeSpot.removeAll(storeSpot); segment.removeAll(segment); isspot=true; invalidate(); break; } return true;
接口设计。
public void setOnCircleCanvasOver(OnCircleCanvasOver oncirclecanvasover) { this.oncirclecanvasover=oncirclecanvasover; } public OnCircleCanvasOver getOnCircleCanvasOver() { return oncirclecanvasover; } public List<SpotXY> getStoreSpot() { return storeSpot; } public void setStoreSpot(List<SpotXY> storeSpot) { this.storeSpot = storeSpot; } public void setPwd() { pwd="123654789"; oncirclecanvasover.Over(pwd); }
接口实现在MainActivity里面
cecs=(CircleCanvas)findViewById(R.id.a); cecs.setOnCircleCanvasOver(new OnCircleCanvasOver() { public void Over(String pwd) { StringBuilder sb=new StringBuilder(); List<SpotXY> list=cecs.getStoreSpot(); for(SpotXY xy:list) { sb.append(xy.getId()); } if(pwd.equals(sb.toString())) { Toast.makeText(getApplication(), "登陆成功"+pwd, Toast.LENGTH_LONG).show(); }else { Toast.makeText(getApplication(), "两次输入不一样"+sb.toString(), Toast.LENGTH_LONG).show(); } } }); }
好了就到这里了,也不早了,早上还要上班。代码设计的不是很好,下次会改进。如有错误的地方请大家指点。谢谢!下面有源码
转载自原文链接, 如需删除请联系管理员。
原文链接:完美的实现九宫格锁屏,转载请注明来源!