一、介绍如何实现小球的往复运动,实现原理
1、View 类定义了一组 invalidate()方法,该方法有好几个版本:
- public void invalidate()
- public void invalidate(int l, int t, int r, int b)
- public void invalidate(Rect dirty)
invalidate()用于重绘组件,不带参数表示重绘整个视图区域,带参数表示重绘指定的区域。
如果要去追溯该方法的源码,大概就是将重绘请求一级级往上交到 ViewRoot,调用 ViewRoot的 scheduleTraversals()方法重新发起重绘请求,scheduleTraversals()方法会发送一个异步消息,调用 performTraversals()方法执行重绘,而 performTraversals()方法最终调用 onDraw()方法。所以,简单来说,调用 View 的 invalidate()方法就相当于调用了 onDraw()方法,
而 onDraw()方法中就是我们编写的绘图代码。
如果要刷新组件或者让画面动起来,我们只需调用 invalidate()方法即可。通过改变数据来影响绘制结果,这是实现组件刷新或实现动画的基本思路。
invalidate()方法只能在 UI 线程中调用,如果是在子线程中刷新组件,View 类还定义了另一组名为 postInvalidate 的方法:
- public void postInvalidate()
- public void postInvalidate(int left, int top, int right, int bottom)
二、接下来我们就创建自定义View实现小球的往复运动
1、首先创建自定义View类BallMoveView
/** * 球往复运动 */ public class BallMoveView extends View { //小球的水平位置 private int x; //小球的垂直位置,固定为100 private static final int Y = 100; //小球的半径 private static final int RADIUS = 30; //小球的颜色 private static final int COLOR = Color.RED; //声明画笔对象 private Paint paint; //移动的方向 private boolean direction; //该构造函数会在代码里面new的时候调用 //当不需要使用xml声明或者不需要使用inflate动态加载的时候,实现此构造函数即可 public BallMoveView(Context context) { super(context); } //在布局layout中使用时调用 //在布局文件中定义了该组件,则会调用此构造方法来创建对象 // 当需要在xml中声明此控件,则需要实现此构造函数。 // 并且在构造函数中把自定义的属性与控件的数据成员连接起来。 public BallMoveView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); //初始化画笔,参数表示抗锯齿 paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(COLOR); x = RADIUS; } //接受一个style资源 public BallMoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //根据x,y的坐标值画一个小球 canvas.drawCircle(x, Y, RADIUS, paint); //改变 x 坐标的值,调用 invalidate()方法后, //小球将因 x 的值发生改变而产生移动的效果 int width = this.getMeasuredWidth();//获取组件的宽度 //小球的水平位置 x 值小于等于小球的半径,说明小球已到达左边边 //界 if (x <= RADIUS) { direction = true; } //小球的水平位置 x 值大于等于组件的宽度减去小球的半径, // 说明小球已到达右边边界 if (x >= width - RADIUS) { direction = false; } x = direction ? x + 5 : x - 5; } }
2、在对应的activity_view1.xml布局文件中引用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".View1Activity"> <com.example.alertdialog.view.BallMoveView android:id="@+id/ballMoveView" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
3、在 View1Activity 中恰恰是通过定时器周期性调用了 invalidate()方法不断重绘组件,也就是不断调用 onDraw()方法,因为小球的位置由 x 来决定,onDraw()每调用一次,x 的值就会变化一次,小球绘制的位置自然也会跟着一起改变,最后形成了小球移动的效果。
具体代码如下:
public class View1Activity extends AppCompatActivity { private BallMoveView ballMoveView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_view1); ballMoveView = findViewById(R.id.ballMoveView); tv = findViewById(R.id.tv); new Timer().schedule(new TimerTask() { @Override public void run() { ballMoveView.postInvalidate(); } },0,50); //参数二表示:任务执行前的延迟毫秒数,参数三表示:连续任务执行间的时间为50毫秒 } }
注意:
上面代码中,通过 Timer 类定义一个计时器,延时 0 毫秒开始计时,每隔 50 毫秒计时一次。
定时任务类 TimerTask 其实就是一个子线程,所以,不能使用只能运行在 UI 线程中的invalidate()方法而只能调用 postInvalidate()方法来重绘组件。
具体效果如下: