Android 中自定义View,实现小球往复运动

一、介绍如何实现小球的往复运动,实现原理

1、View 类定义了一组 invalidate()方法,该方法有好几个版本:

  1. public void invalidate()
  2. public void invalidate(int l, int t, int r, int b)
  3. public void invalidate(Rect dirty)

invalidate()用于重绘组件,不带参数表示重绘整个视图区域,带参数表示重绘指定的区域。
如果要去追溯该方法的源码,大概就是将重绘请求一级级往上交到 ViewRoot,调用 ViewRoot的 scheduleTraversals()方法重新发起重绘请求,scheduleTraversals()方法会发送一个异步消息,调用 performTraversals()方法执行重绘,而 performTraversals()方法最终调用 onDraw()方法。所以,简单来说,调用 View 的 invalidate()方法就相当于调用了 onDraw()方法,
而 onDraw()方法中就是我们编写的绘图代码。

如果要刷新组件或者让画面动起来,我们只需调用 invalidate()方法即可。通过改变数据来影响绘制结果,这是实现组件刷新或实现动画的基本思路。

invalidate()方法只能在 UI 线程中调用,如果是在子线程中刷新组件,View 类还定义了另一组名为 postInvalidate 的方法:

  1. public void postInvalidate()
  2. 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()方法来重绘组件。

具体效果如下:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lu202032/article/details/123499160