본문 바로가기
안드로이드 자바

[JAVA][Android] 라이브 방송 화면 만들기

by teamnova 2021. 6. 11.
728x90

오늘은 라이브 방송 화면을 만들어 보겠습니다.

 

카메라의 화면을 그냥 보여주면 되는거 아닌가? 싶겠지만

 

자세히 보면 카메라 화면 위에 채팅화면과 좋아요 등등의 뷰가 추가된 걸 보실수 있습니다.

 

이런 화면을 구성하기 위해서 필요한 뷰가 있습니다.

 

바로 SurfaceView인데요

 

SurfaceView는 View를 상속받은 클래스로

 

일반 View는 onDraw 메소드를 시스템에서 자동으로 호출해줌으로써 화면을 그린다면

 

SurfaceView는 그리기를 시스템에 맡기는 것이 아니라 스레드를 이용해 강제로 화면에 그림으로써 원하는 시점에 

 

바로 화면에 그릴 수 있습니다. 

 

SurfaceView는 자기 영역 부분의 Window를 뚫어서(punch) 자신이 보여지게끔 하고 Window와 View가 블렌딩되어 화면

 

에 보여지게 됩니다.

 

 

 

SurfaceView로부터 상속받을 경우 디폴트로 구현해야 할 메소드가 있습니다.

 

  • public void onDraw (Canvas canvas) : 화면을 그린다.
  • public void surfaceChanged() : 뷰가 변경될 때 호출된다.
  • public void surfaceCreated() : 뷰가 생성될 때 호출된다.
  • public void surfaceDestroyed() : 뷰가 종료될 때 호출된다.

https://stickode.com/mainlogin.html

 

STICKODE

 

stickode.com

그럼 진행 순서를 보여드리겠습니다. 

 

1. SurfaceView를 이용하여 CAMERA 사용하기

 

2. LayoutInflater를 이용하여 View를 Overlay하기 

 

 

시작하기 전에 구성해줘야 할 것들 입니다. 

 

Manifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.surfaceview">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Surfaceview">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <uses-permission android:name="android.permission.CAMERA" />

 

build.gradle 

implementation 'gun0912.ted:tedpermission:2.0.0' // 추가해야 하는 부분

권한을 쉽게 해주기 위해 TedPermission을 사용합니다. 

 

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
   
    <Button
        android:id="@+id/startcamerapreview"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="- Start Camera Preview -"
        />
    <Button
        android:id="@+id/stopcamerapreview"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="- Stop Camera Preview -"
        />
    <SurfaceView
        android:id="@+id/surfaceview"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />
</LinearLayout>

 

 

 

MainActivity.java

package com.example.surfaceview;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.Manifest;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

import java.io.IOException;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {

    Camera camera;
    SurfaceView surfaceView;
    SurfaceHolder surfaceHolder;
    boolean previewing = false;
    LayoutInflater controlInflater = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //권한 체크
        TedPermission.with(getApplicationContext())
                .setPermissionListener(permissionListener)
                .setRationaleMessage("카메라 권한이 필요합니다.")
                .setDeniedMessage("카메라 권한을 거부하셨습니다.")
                .setPermissions( Manifest.permission.CAMERA)
                .check();


        Button buttonStartCameraPreview = (Button)findViewById(R.id.startcamerapreview);
        Button buttonStopCameraPreview = (Button)findViewById(R.id.stopcamerapreview);

        getWindow().setFormat(PixelFormat.UNKNOWN);
        surfaceView = (SurfaceView)findViewById(R.id.surfaceview);
        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(this);
        surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

        buttonStartCameraPreview.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                if(!previewing){
                    camera = Camera.open();
                    if (camera != null){
                        try {
                            camera.setPreviewDisplay(surfaceHolder);
                            camera.startPreview();
                            previewing = true;
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            }});

        buttonStopCameraPreview.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                if(camera != null && previewing){
                    camera.stopPreview();
                    camera.release();
                    camera = null;

                    previewing = false;
                }
            }});

    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        camera = Camera.open();
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
// TODO Auto-generated method stub
        if(previewing){
            camera.stopPreview();
            previewing = false;
        }

        if (camera != null){
        
            try {
                camera.setPreviewDisplay(surfaceHolder);
                camera.startPreview();
                previewing = true;
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        // TODO Auto-generated method stub
        camera.stopPreview();
        camera.release();
        camera = null;
        previewing = false;
    }

    PermissionListener permissionListener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            Toast.makeText(getApplicationContext(), "권한이 허용됨", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(getApplicationContext(), "권한이 거부됨", Toast.LENGTH_SHORT).show();
        }
    };

}

 

SurfaceView에서 버튼 이벤트를 통해 Camera 시작 / 중지 합니다.

 

surfaceHolder.callback Method인 surfaceCreated(), surfaceChange() 및 surfaceDestroyed() 함수를 처리합니다.

 

 

 

 

그러데 카메라를 사용하여 화면이 나오긴 하는데 이상합니다. 화면이 90도로 돌아가 있는 경우가 생깁니다. 

 

화면을 바로 잡아줘야 겠죠? 

 

 

            int rotation = getWindowManager().getDefaultDisplay().getRotation();

            int degrees = 0;

            switch (rotation) {

                case Surface.ROTATION_0: degrees = 0; break;

                case Surface.ROTATION_90: degrees = 90; break;

                case Surface.ROTATION_180: degrees = 180; break;

                case Surface.ROTATION_270: degrees = 270; break;

            }

            int result  = (90 - degrees + 360) % 360;

            camera.setDisplayOrientation(result);

 

화면의 기울기를 계산하여 바로 잡아주는 코드를 넣어줍니다. (화면을 보여주기 전에 처리해야 겠죠? 코드는 밑에서 확인하세요 ^^)

 

 

그러면 화면이 잘 나오는걸 확인할 수 가 있는데요

 

 

이 화면위에 

 

저희가 원하는 채팅창을 위에 올려줄까요?

 

이를 OverLay라고 하는데요. 뜻을 보니 대충 감이 오시죠?

 

 

overlay할 화면을 구성해 줍니다. 

 

 

control.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:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">


    <TextView
        android:id="@+id/textView"
        android:layout_width="200dp"
        android:layout_height="150dp"
        android:layout_marginTop="460dp"
        android:background="#59A1DFCC"
        android:textSize="15sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/et_text"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/sendMsg"
        app:layout_constraintHorizontal_bias="0.879"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView"
        app:layout_constraintVertical_bias="0.464" />

    <Button
        android:id="@+id/sendMsg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_margin="10px"
        android:layout_marginTop="9dp"
        android:layout_marginEnd="4dp"
        android:layout_marginRight="4dp"
        android:text=" 채팅 보내기 "
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView"
        app:layout_constraintVertical_bias="0.428" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

기존의 코드에서 LayoutInflater를 활용해 overlay 해줍니다. 

        controlInflater = LayoutInflater.from(getBaseContext());
        View viewControl = controlInflater.inflate(R.layout.control, null);
        ViewGroup.LayoutParams layoutParamsControl
                = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                ViewGroup.LayoutParams.FILL_PARENT);
        this.addContentView(viewControl, layoutParamsControl);

 

 

EditText에 입력한 글을 버튼을 누르면 TextView에 출력해주는 채팅창의 로직을 구성해 주면 

 

완성됩니다. 

 

 

MainActivity.java

package com.example.surfaceview;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.Manifest;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

import java.io.IOException;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {

    Camera camera;
    SurfaceView surfaceView;
    SurfaceHolder surfaceHolder;
    EditText editText;
    Button btnSend;
    TextView textView;
    boolean previewing = false;
    LayoutInflater controlInflater = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //권한 체크
        TedPermission.with(getApplicationContext())
                .setPermissionListener(permissionListener)
                .setRationaleMessage("카메라 권한이 필요합니다.")
                .setDeniedMessage("카메라 권한을 거부하셨습니다.")
                .setPermissions( Manifest.permission.CAMERA)
                .check();


        Button buttonStartCameraPreview = (Button)findViewById(R.id.startcamerapreview);
        Button buttonStopCameraPreview = (Button)findViewById(R.id.stopcamerapreview);

        getWindow().setFormat(PixelFormat.UNKNOWN);
        surfaceView = (SurfaceView)findViewById(R.id.surfaceview);
        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(this);
        surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

        controlInflater = LayoutInflater.from(getBaseContext());
        View viewControl = controlInflater.inflate(R.layout.control, null);
        ViewGroup.LayoutParams layoutParamsControl
                = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                ViewGroup.LayoutParams.FILL_PARENT);
        this.addContentView(viewControl, layoutParamsControl);

        editText = findViewById(R.id.et_text);
        btnSend = findViewById(R.id.sendMsg);
        textView = findViewById(R.id.textView);

        btnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), "Button을 눌렀습니다.",Toast.LENGTH_SHORT).show();
                String message = editText.getText().toString();

                if(message.trim().length() > 0) {
                    textView.setText(message);
                    editText.getText().clear();
                } else {
                    Toast.makeText(getApplicationContext(), "메시지를 입력하세요.",Toast.LENGTH_SHORT).show();
                }
            }
        });

        buttonStartCameraPreview.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                if(!previewing){
                    camera = Camera.open();
                    if (camera != null){
                        try {
                            camera.setPreviewDisplay(surfaceHolder);
                            camera.startPreview();
                            previewing = true;
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            }});

        buttonStopCameraPreview.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                if(camera != null && previewing){
                    camera.stopPreview();
                    camera.release();
                    camera = null;

                    previewing = false;
                }
            }});

    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        camera = Camera.open();
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
		// TODO Auto-generated method stub
        if(previewing){
            camera.stopPreview();
            previewing = false;
        }

        if (camera != null){

            int rotation = getWindowManager().getDefaultDisplay().getRotation();
            int degrees = 0;

            switch (rotation) {
                case Surface.ROTATION_0: degrees = 0;
                break;

                case Surface.ROTATION_90: degrees = 90;
                break;

                case Surface.ROTATION_180: degrees = 180;
                break;

                case Surface.ROTATION_270: degrees = 270;
                break;
            }

            int result  = (90 - degrees + 360) % 360;

            camera.setDisplayOrientation(result);


            try {
                camera.setPreviewDisplay(surfaceHolder);
                camera.startPreview();
                previewing = true;
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        // TODO Auto-generated method stub
        camera.stopPreview();
        camera.release();
        camera = null;
        previewing = false;
    }

    PermissionListener permissionListener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            Toast.makeText(getApplicationContext(), "권한이 허용됨", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(getApplicationContext(), "권한이 거부됨", Toast.LENGTH_SHORT).show();
        }
    };

}

 

 

완성된 앱을 보시면 

 

마치 실제 스트리밍 방송에서 실시간 채팅을 할 때와 

 

비슷한 화면이 구성되어 있는것을 확인하실수 있습니다.