오늘은 라이브 방송 화면을 만들어 보겠습니다.
카메라의 화면을 그냥 보여주면 되는거 아닌가? 싶겠지만
자세히 보면 카메라 화면 위에 채팅화면과 좋아요 등등의 뷰가 추가된 걸 보실수 있습니다.
이런 화면을 구성하기 위해서 필요한 뷰가 있습니다.
바로 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
그럼 진행 순서를 보여드리겠습니다.
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();
}
};
}
완성된 앱을 보시면
마치 실제 스트리밍 방송에서 실시간 채팅을 할 때와
비슷한 화면이 구성되어 있는것을 확인하실수 있습니다.
'안드로이드 자바' 카테고리의 다른 글
[JAVA][Android] intent를 사용하여 이미지 보내기 (2) | 2021.06.16 |
---|---|
[JAVA][Android] 안드로이드 OCR 기능 만들기 (4) | 2021.06.13 |
[JAVA][android] WiFi정보 스캔 빠르게 구현하기 (0) | 2021.06.09 |
[JAVA][Android] 로또 QR 코드 웹뷰 띄워주기 (0) | 2021.06.08 |
[JAVA][Android] 안드로이드 STT (4) | 2021.06.06 |