[Android] 안드로이드

[ 안드로이드 응용 ] 바운스 볼(공 튀기기) 구현하기

개발자혜콩 2026. 4. 15. 10:54

1. SurfaceView

SurfaceView를 활용한 바운스 볼(공 튀기기) 구현하기

목표: SurfaceView와 별도의 렌더링 스레드를 사용하여, 우주 배경 위에서 화면 경계에 부딪히면 튕겨 나오는 공 애니메이션을 구현하시오.

 

[세부 조건]

  1. 화면 설정 (MainActivity): 기본 XML 레이아웃을 사용하지 않고, 타이틀바(Title Bar)를 제거한 뒤 커스텀 뷰(GameView)를 메인 화면으로 꽉 채워 설정한다.
  2. 객체 분리 (Ball.java): 공의 상태(위치, 크기, 이동 속도, 이미지)와 동작(그리기, 이동, 벽 충돌 처리)을 관리하는 독립적인 클래스를 작성한다.
    • 이동 중 화면 경계(SurfaceFrame)의 상/하/좌/우 끝에 도달하면 이동 방향(delta)에 -1을 곱해 반대로 튕기게 만든다.
  3. SurfaceView 구현 (GameView.java): SurfaceView를 상속받고 SurfaceHolder.Callback을 구현하여 생명주기를 관리한다.
  4. 게임 루프(스레드) 구성: 메인 UI 스레드가 아닌 별도의 스레드를 생성해 무한 루프를 돌며 다음 작업을 고속으로 반복한다.
    • 공의 좌표 업데이트 (이동 및 충돌 계산)
    • Canvas 잠금 (lockCanvas())
    • 우주 배경 이미지(bg_space)를 화면 크기에 맞춰 그리고, 그 위에 공(red_ball)을 그림
    • Canvas 잠금 해제 및 화면 갱신 (unlockCanvasAndPost())
  5. 초기 세팅: 공의 크기는 100x100으로 설정하고, 좌표 (0,0)에서 X축으로 15, Y축으로 30의 속도(delta)로 출발시킨다. 앱이 종료되면(surfaceDestroyed) 스레드도 안전하게 종료되도록 플래그(goOnPlay)를 처리한다.

▼결과화면

 

▼activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 


▼MainActivity.java

package kr.ac.dju.surfaceview;

import android.os.Bundle;
import android.view.Window;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        requestWindowFeature(Window.FEATURE_NO_TITLE);

        setContentView(new GameView(this));
    }
}

 

 

▼GameView.java

package kr.ac.dju.surfaceview;

import static android.content.ContentValues.TAG;

//import static androidx.appcompat.graphics.drawable.DrawableContainerCompat.Api21Impl.getResources;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import org.jspecify.annotations.NonNull;

public class GameView extends SurfaceView
        implements SurfaceHolder.Callback
{
    private static final String TAG = GameView.class.getName();
    private final SurfaceHolder holder;
    private boolean goOnPlay = true;
    private Ball ball;
    private Thread renderer = new Thread() {
        @Override // 부모 메소드 재정의
        public void run() {
            super.run();
            Drawable bg = getResources().getDrawable(R.drawable.bg_space);
            bg.setBounds(holder.getSurfaceFrame());
            ball.setDelta(15,30);
            while(goOnPlay) {
                ball.move(holder.getSurfaceFrame());
                Canvas canvas = holder.lockCanvas();
                bg.draw(canvas);
                ball.draw(canvas);
                holder.unlockCanvasAndPost(canvas);
            }
        }
    };

    //생성자
    public GameView(Context context) {
        super(context);

        Log.i(TAG,"GameView created");
        holder = getHolder();
        holder.addCallback(this);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        renderer.start();
        ball = new Ball();
        ball.setImage(getResources().getDrawable(R.drawable.red_ball));
        ball.setSize(new Point(100, 100));
        ball.setPoint(new Point(0,0));
    }
    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {

    }
    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        goOnPlay = false; // kill thread
    }
}

 

▼Ball.java

package kr.ac.dju.surfaceview;

import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;

public class Ball {
    private Drawable image = null;
    private Point point = new Point();
    private Point size = new Point();
    private Point delta;

    public Drawable getImage() {
        return image;
    }
    public void setImage(Drawable image) {
        this.image = image;
    }
    public Point getPoint() {
        return point;
    }
    public void setPoint(Point point) {
        this.point = point;
    }
    public Point getSize() {
        return size;
    }
    public void setSize(Point size) {
        this.size = size;
    }
    public void draw(Canvas canvas) {
        image.setBounds(point.x, point.y, point.x+size.x, point.y+size.y);
        image.draw(canvas);
    }
    public void setDelta(int dx, int dy) {
        delta = new Point(dx,dy);
    }
    public void move(Rect surfaceFrame) {
// X axis collision
        if(point.x + delta.x <0 ||
                point.x + delta.x +size.x>surfaceFrame.right) delta.x*=-1;
        else point.x +=delta.x;
// Y axis collision
        if(point.y + delta.y <0 ||
                point.y + delta.y +size.y>surfaceFrame.bottom) delta.y*=-1;
        else point.y +=delta.y;
    }
}