安卓第二趴

今天是对昨天知识的补充

加上新的学习内容

1. 昨天的内容里,有的地方没有说的很清楚,今天加以补充。

在说到背景图的添加的时候,只是简单的说了放在drawable文件下。但是没有说明怎么在代码中实现。

我们知道,我们所有的操作实际上就是在一个画布上搞来搞去,那么背景图,既然要作为大背景,理应放在一个大的布局范围。

我们放在RelativeLayout中:

图片

这样我们就可以看到效果图了:

图片

(和昨天的图不一样,因为我换了啊,我还会换的,看心情。。。)

2.     View是我们自定义的。

这里的view相当于是在整个大的画布上圈出了一个范围,画过画的人都知道,所有的画都不要从一张纸的边缘处就下笔,要有留白。

我们接下来的操作,比如说绘制网格等等就在这个圈出来的部分进行。

昨天我们只是简单提了一下view定义了基本的绘图操作,这里我们详细说明一下:

基本操作由三个函数完成:draw()、measure()、layout()。其内部又包含了三个子方法:onDraw()、onMeasure()、onLayout()

具体操作如下:

measure操作

     measure操作主要用于计算视图的大小,即视图的宽度和长度。在view中定义为final类型,要求子类不能修改。measure()函数中又会调用下面的函数:onMeasure(),视图大小的将在这里最终确定,也就是说measure只是对onMeasure的一个包装,子类可以覆写onMeasure()方法实现自己的计算视图大小的方式,并通过setMeasuredDimension(width, height)保存计算结果。

layout操作

     layout操作用于设置视图在屏幕中显示的位置。在view中定义为final类型,要求子类不能修改。layout()函数中有两个基本操作:

     (1)setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,该函数用于将这些参数保存起来;

     (2)onLayout(),在View中这个函数什么都不会做,提供该函数主要是为viewGroup类型布局子视图用的;

draw操作

     draw操作利用前两步得到的参数,将视图显示在屏幕上,到这里也就完成了整个的视图绘制工作。子类也不应该修改该方法,因为其内部定义了绘图的基本操作:

     (1)绘制背景;

     (2)如果要视图显示渐变框,这里会做一些准备工作;

     (3)绘制视图本身,即调用onDraw()函数。在view中onDraw()是个空函数,也就是说具体的视图都要覆写该函数来实现自己的显示(比如TextView在这里实现了绘制文字的过程)。而对于ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的,其包含了多个子view,而子View已经实现了自己的绘制方法,因此只需要告诉子view绘制自己就可以了,也就是下面的dispatchDraw()方法;

     (4)绘制子视图,即dispatchDraw()函数。在view中这是个空函数,具体的视图不需要实现该方法,它是专门为容器类准备的,也就是容器类必须实现该方法;

     (5)如果需要(应用程序调用了setVerticalFadingEdge或者setHorizontalFadingEdge),开始绘制渐变框;

     (6)绘制滚动条;

      从上面可以看出自定义View需要最少覆写onMeasure()和onDraw()两个方法。

    还记得我们在之前讲JAVA的时候说过,抽象父类的所有方法在子类中都要实现,这里有异曲同工之妙。

下面我们再聊一聊onMeasure():

onMeasure(int widthMeasureSpec, int heightMeasureSpec)中,两个参数的作用:        widthMeasureSpec和heightMeasureSpec这两个int类型的参数,看名字应该知道是跟宽和高有关系,但它们其实不是宽和高,而是由宽、高和各自方向上对应的模式来合成的一个值:其中,在int类型的32位二进制位中,31-30这两位表示模式,0~29这三十位表示宽和高的实际值.

其中模式一共有三种,被定义在Android中的View类的一个内部类中:View.MeasureSpec:

①UNSPECIFIED:表示默认值,父控件没有给子view任何限制。------二进制表示:00

②EXACTLY:表示父控件给子view一个具体的值,子view要设置成这些值的大小。

----二进制表示:01

③AT_MOST:表示父控件个子view一个最大的特定值,而子view不能超过这个值的大小。------二进制表示:10

那么问题来了,MeasureSpec是什么?

MeasureSpe描述了父View对子View大小的期望.里面包含了测量模式和大小.我们可以通过以下方式从MeasureSpec中提取模式和大小,该方法内部是采用位移计算.

int specMode = MeasureSpec.getMode(measureSpec);//得到模式

int specSize = MeasureSpec.getSize(measureSpec);//得到大小

也可以通过MeasureSpec的静态方法把大小和模式合成,该方法内部只是简单的相加.

MeasureSpec.makeMeasureSpec(specSize,specMode);

    每个View都包含一个ViewGroup.LayoutParams类或者其派生类,LayoutParams中包 含了View和它的父View之间的关系,而View大小正是View和它的父View共同决定的。

我们平常使用类似于RelativeLayout和LinearLayout的时候,在其内部添加view的时候,不管是布局文件中加入还是在代码中使用addView方法添加,实际上都会调用这个onMeasure方法,而measure和onMeasure中的两个参数,是由各级父控件往子控件/子view进行一层层传递的。我们可以在xml中定义Layout的宽和高的具体的值或宽高的填充方式:matchparent/wrapcontent,也可以在代码中使用LayoutParams设置,而实际上这里设置的值就会对应到上面的measure和onMeasure方法中的两个参数的模式,对应关系如下:

具体的值(如width=200dp)和matchparent/fillparent,对应模式中的MeasureSpec.EXACTLY

包裹内容(width=wrapcontent)则对应模式中的MeasureSpec.AT_MOST。

一个view的宽高尺寸,只有在测量之后才能得到,也就是measure方法被调用之后。具体实现代码如下:

图片

第一个红框框就是我们上面说的模式和大小组合才能知道这个view的大小。第二个红框框是进行计算并保存计算结果。这里我们想要绘制的是一个正方形,所以取了宽和高中最小值作为width。中间的if和else if是判断模式的,因为模式可能在设置的时候出现问题,所以这里进行简单处理。

.    现在我们绘制棋盘,那我们需要知道棋盘的高度和宽度,以及每行格子数。那这些数据在哪里进行初始化呢?我们选择在onSizeChanged()中进行设置。使用这个是因为当布局发生变化时,可以回调onMeasure重新布局。具体代码如下:

图片

至于绘制操作我们肯定是放在onDraw()里面,我们使用了一个drawBoard()方法。昨天说过,绘制的时候是需要paint的,所以定义一下paint,此外还要在init中进行初始化(大小,风格,抗锯齿等等)。具体的代码我们昨天已经看过,这里就不再看了。这里就是要注意一下棋盘画的时候应该注意线的初始坐标和结束坐标,还有纵坐标的变化。

    绘制完棋盘我们就应该绘制棋子了,但是棋子的位置我们并不确定,因为这是和用户交互的,所以我们肯定要在onTouchEvent()函数中写交互。但是在这之前,我们要在代码中引入这两个棋子的图片(注意,这里是代码中引入,和昨天的是不一样的)。

    ·首先得声明一下两个棋子:

图片

mWhitePiece是我们棋子的名字。

然后进行棋子的初始化(在init中):

图片

至于书写格式,大家模仿就可以。

    接下来就是棋子的绘制了。这里要注意的:第一点,要注意棋子不能比我们绘制的格子还要大,大小得适宜;第二点,在绘制棋子的时候要考虑到两个棋子之间的空隙,不能交叉,也不要紧贴,美观一些。这就要求我们在绘制的时候一定多注意坐标的设置。

    为了满足上面两点,我们定义一个比例变量,让棋子等于格子宽的3/4:

图片

那么这里我们就又涉及到棋子尺寸大小的改变了。上面提到过了,布局改变放在onSizeChanged()中:

图片

棋子的宽度我们是行高乘以3/4比例。红框下面两行是对白棋和黑棋的绘制,我们使用的是BitMap里的createScaledBitmap方法。

下面讲棋子与用户的交互:

先介绍一下运动事件:

图片

再详细介绍一下onTouchEvent()方法:

  • public boolean onTouchEvent (MotionEvent event) 

       参数event:参数event为手机屏幕触摸事件封装类的对象,其中封装了该事件的所有信息,例如触摸的位置、触摸的类型以及触摸的时间等。该对象会在用户触摸手机屏幕时被创建。
       返回值:该方法的返回值机理与键盘响应事件的相同,同样是当已经完整地处理了该事件且不希望其他回调方法再次处理时返回true,否则返回false。
       该方法并不像之前介绍过的方法只处理一种事件,一般情况下以下三种情况的事件全部由onTouchEvent方法处理,只是三种情况中的动作值不同。
       屏幕被按下:当屏幕被按下时,会自动调用该方法来处理事件,此时MotionEvent.getAction()的值为MotionEvent.ACTION_DOWN,如果在应用程序中需要处理屏幕被按下的事件,只需重新该回调方法,然后在方法中进行动作的判断即可。
       屏幕被抬起:当触控笔离开屏幕时触发的事件,该事件同样需要onTouchEvent方法来捕捉,然后在方法中进行动作判断。当MotionEvent.getAction()的值为MotionEvent.ACTION_UP时,表示是屏幕被抬起的事件。
       在屏幕中拖动:该方法还负责处理触控笔在屏幕上滑动的事件,同样是调用MotionEvent.getAction()方法来判断动作值是否为MotionEvent.ACTION_MOVE再进行处理。

看上面的介绍我们知道了,在写五子棋的时候,如果Action是Down形式的,那么手滑点歪的情况不容易解决,当然,用户更不可能在滑动的过程产生落子的行为(噗,,,突然污了,咳咳咳),所以我们使用的是UP形式。要注意,当判断可以处理该行为的时候,返回值应该设为true,上面对于返回值的介绍很清楚了。

    那么这个事件怎么处理呢?用户落子(我不行了,这个我可以笑好久),我们索要的有效信息是点击的那个坐标。所以我们在设一个全局变量来记录棋子坐标:

图片

这种定义形式需要学习。

此外,我们还需要有一个变量来告诉我们到底是黑子在下还是白棋在下:

图片

用布尔形式即可。那这句话的含义就是黑子先手或者是黑子正在下。

接下来我们就该接受用户点击的坐标值并进行是否落子的判断了。

图片

  • 第一点我们比较容易想到,就是简单粗暴的直接获取横纵坐标;

  • 第二点是说写一个方法,把这个点封装起来,放到Point类的p中,而且大家看这个方法是获得有效的坐标值,那有效的意思是什么呢?我们知道,有的人手粗,有的人手细,有的人就是喜欢用脚或者胳膊肘下棋,还有的人眼睛就是不大好使,那问题就来了,我怎么着都点不到那个行与列的交叉处,接近的位置难道就不认了?为了满足用户的这个需求,我们写一个方法,获得有效的坐标值:

    图片

    代码很简单,就是取整来减少误差。只要不是手太粗、胳膊太粗、脚太粗、眼睛太不好使都不会有什么问题的。

  • 第四点我们也比较容易想到,得到了坐标值,就把棋子添加进去啊,这里就是判断一下是黑子下还是白子下。与之有关的是,当我们下完一个子之后,要把棋子的状态恢复到原来的状态去,也就是第五点的第二行的含义。

  • 在最后不要忘记了一定要加上第五点的invalidate(),用这个函数进行重绘;

  • 那么第三点是什么呢?实际上就是考虑是否全面的问题:用户不可以在同一个点处重复落子。

那这里与用户的交互就完成了,下来我们进行棋子的绘制:

图片

这里代码看起来多,但是重复性很大,黑子白子逻辑一样的,绘制棋子这一块比较麻烦的是坐标值得确定,要考虑间隙,考虑与格子的关系,大家可以自行画图对照代码进行理解,这里我就不赘述了,不好说啊。。。

给大家看一点效果:(背景图我很喜欢,但是当五子棋的背景图真是丑啊,棋子也没有进行抠图,凑活一下吧先)

视频地址:https://v.qq.com/x/page/u132224hol2.html

006


原创不易,请多多支持与交流~

猜你喜欢

转载自blog.csdn.net/allein_STR/article/details/113987476