일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- launchscreen
- Android
- 3b52.1
- react
- 360도 이미지 뷰어
- 360도 이미지
- 라이브러리 없이
- 파노라마 뷰
- SwiftUI
- 명시적 정체성
- ssot
- 리액트
- native
- 뷰 생명주기
- React Native
- 구조적 정체성
- 뷰 정체성
- 360도 뷰어
- 리액트 네이티브
- privacyinfo.plist
- 앱 성능 개선
- react-native-fast-image
- 스켈레톤 통합
- ios
- React-Native
- data driven construct
- launch screen
- panorama view
- 네이티브
- requirenativecomponent
- Today
- Total
Neoself의 기술 블로그
React Native 네이티브 모듈 연결하기 - 파노라마 뷰(Android 3/3) 본문
현재까지 프로젝트root/android 폴더 내 파일들을 설명하자면 다음과 같다.
- React Native 초기 생성 때 기본으로 추가되는 MainActivity.kt, MainApplication.kt
- gvr-android-sdk를 모듈화하여 RN에 연결하기 위한 PanoramaViewManager.java 및 PanoramaViewPackage.java 파일
- gvr-android-sdk가 제공하는 패키지 및 함수가 호출되고 사용되는 파일들이 위치한 video360 폴더
- MediaLoader.java = 렌더되는 파노라마 이미지 처리 및 관리 관련 클래스가 구현되어있는 파일
- VideoUiView.java = 렌더 대상을 이미지와 비디오 사이에서 토글하기 위한 Ui 구현에 필요한 파일
- MonoscopicView.java = 하나의 시점으로 360도 이미지를 볼 수 있는 View 컴포넌트, 즉 파노라마뷰 클래스가 구현된 파일
- 미디어파일을 파노라마뷰에 맞추기 위해 구형형태로 펴주는? createUvSphere(), 파노라마 뷰 구현에 사용되는 OpenGL 프로그램을 호출시키는 compileProgram() 등의 함수들이 정의되어있는 rendering 폴더
위와 같이 파일들을 추가하여 네이티브 모듈 연결을 완료한 후, 네이티브 모듈을 본래 기획에 맞게 수정하는 과정을 거쳤다. gvr-android-sdk 구현코드와 본래기획과 다른 점은 다음와 같다.
본래 기획 | gvr-android-sdk |
RN에서 imageURL을 통해 파나로마 이미지 전달 | Android Intent를 통해 이미지 파일 전달 |
핀치 제스처를 통해 줌인, 줌아웃 기능 필요 | 핀치줌 제스처 관련 로직 없음 |
터치 제스처로만 화면 각도 변경 | 기기 회전각도와 드래그 제스처 둘다 화면 각도에 영향 |
1. React Native 컴포넌트 속성값으로 파노라마 이미지 파일 전달되게 로직 변경
가장 먼저, MediaLoader 파일 내부에 선언된 MediaLoaderTask 클래스를 수정하였다. 기존 로직의 경우 Intent 내부에 첨부된 imageURI를 통해 이미지 파일 전달이 이루어졌는데, RN에서 전달받은 String 인자로 이미지 해석 및 load가 되게 코드를 변경하였다.
import ...
public class MediaLoader
...
// string 타입의 imageUrl을 RN으로부터 받아, 이를 수정한 MediaLoaderTask에 삽입하는 함수 추가
public void setImage(String imageUrl){
mediaLoaderTask = new MediaLoaderTask();
mediaLoaderTask.execute(imageUrl);
}
// Intent를 수신받을 때 실행되는 대신, setImage 함수가 외부에서 호출될때 실행되도록 변경
public class MediaLoaderTask extends AsyncTask<String, Void, Boolean> {
@Override
protected Boolean doInBackground(String... urls) {
String value = urls[0];
if (isCancelled()) {
return null;
}
InputStream istr = null;
try {
// 1. 인자로 전달받은 url string값을 Uri로 parse
Uri imageUri = Uri.parse(value);
String scheme = imageUri.getScheme();
// 2. InputStream으로 변환
if (scheme == null || scheme.equalsIgnoreCase(SCHEME_FILE)) {
istr = new FileInputStream(new File(imageUri.getPath()));
} else {
URL url = new URL(value);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
istr = connection.getInputStream();
}
Assertions.assertCondition(istr != null);
// 3. InputStream을 Bitmap 이미지파일로 decode한후 클래스 변수인 mediaImage로 저장
mediaImage = decodeSampledBitmap(istr);
} catch (Exception e) {
if (isCancelled()) {
return null;
}
Log.e("MediaLoader", "Could not load file: " + e);
return null;
}
// 4. 클래스 변수인 mesh로 Monoscopic UvSphere 저장
mesh = Mesh.createUvSphere(
SPHERE_RADIUS_METERS, DEFAULT_SPHERE_ROWS, DEFAULT_SPHERE_COLUMNS,
DEFAULT_SPHERE_VERTICAL_DEGREES, DEFAULT_SPHERE_HORIZONTAL_DEGREES,
Mesh.MEDIA_MONOSCOPIC);
// 5. displayWhenReady 호출
displayWhenReady();
return false;
}
...
@AnyThread
private synchronized void displayWhenReady() {
...
if (mediaImage != null) {
// 6. 캔버스 객체에 클래스변수 mediaImage에 저장된 이미지 파일 표시
displaySurface = sceneRenderer.createDisplay(
mediaImage.getWidth(), mediaImage.getHeight(), mesh);
Canvas c = displaySurface.lockCanvas(null);
c.drawBitmap(mediaImage, 0, 0, null);
displaySurface.unlockCanvasAndPost(c);
} else {
...
}
}
...
}
이제 ImageUrl String값으로 이미지파일을 생성하는 MediaLoaderTask 함수가 완성되었으니 React Native 인자값을 받을 때 해당 함수가 호출되도록 연결해보자.
가장 먼저, MediaLoader 클래스에 접근이 가능한 MonoscopicView 클래스 내부에 setImageSource 함수를 생성하여, mediaLoader 내부에 접근 & MediaLoaderTask 함수를 호출할 수 있도록 연결한다.
import ...
public final class MonoscopicView extends GLSurfaceView {
private MediaLoader mediaLoader;
private Renderer renderer;
private TouchTracker touchTracker;
public MonoscopicView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
setPreserveEGLContextOnPause(true);
}
public void setDimensions(ReadableMap dimensions) {
mediaLoader.setDimensions(dimensions);
}
// 여기! MediaLoader 클래스가 private으로 선언되어 MonoscopicView를 거쳐가게 로직을 설계하였는데
// 지금 돌아보면 Manager파일에서 바로 MediaLoader 내부 메서드를 실행하게 변경이 필요해보인다.
public void setImageSource(String value){
mediaLoader.setImage(value);
}
PanoramaViewManager 파일 상에는 @ReactProp() 어노테이션을 통해 어노테이션 내부 선언된 name과 동일한 React Native 단 컴포넌트 속성이 존재할 경우, 전달받은 속성값을 매개체로 함수를 실행할 수 있다. 이를 활용해 RN에서 전송하는 imageUrl을 핸들링하는 imageUrl 함수를 추가하였다.
또한 이제 Intent를 통한 이미지 파일 전송 로직을 사용하지 않기에 PanoramaViewManager initialize 단계에 실행되는 intent 핸들링 로직을 일괄 제거해주었다.
// PanoramaViewManager
public class PanoramaViewManager extends SimpleViewManager<MonoscopicView> implements LifecycleEventListener {
private static final String REACT_CLASS = "PanoramaView";
@NonNull
@Override
public String getName(){ return REACT_CLASS; }
public MonoscopicView videoView;
public PanoramaViewManager(ReactApplicationContext context) {
context.addLifecycleEventListener(this);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
videoView = (MonoscopicView) inflater.inflate(R.layout.video_activity, null);
videoView.initialize();
// 아래 구문 모두 제거
//Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
//intent.setClassName("com.google.vr.sdk.samples.video360", "com.google.vr.sdk.samples.video360.VrVideoActivity");
//intent.setData(Uri.parse("file:///sdcard/IMAGE.JPG"));
//intent.putExtra("stereoFormat", 2);=
//videoView.loadMedia(intent);
}
// RN 네이티브 컴포넌트의 imageUrl이라는 속성 존재시, imageUrl 함수 실행
@ReactProp(name = "imageUrl")
public void imageUrl( MonoscopicView view, String imageUrl) {
view.setImageSource(imageUrl);
}
@ReactProp(name = "dimensions")
public void setDimensions(MonoscopicView view, ReadableMap dimensions) {
view.setDimensions(dimensions);
}
상기 절차들을 모두 진행하고 나면, 아래와 같이 React Native 단에서 호출하는 PanoramaView 컴포넌트의 인자를 통해 렌더 이미지 링크를 직접 전달할 수 있게 된다.
// React Native
return (
<PanoramaView
imageUrl={
'https://live.staticflickr.com/4066/5147559690_54a4024c80_b.jpg'
}
style={{height: '100%'}}
/>
);
2. 핀치 제스처를 통해 줌인, 줌아웃 할 수 있는 기능 추가
기존 MonoscopicView.java 파일 내부 제스처 핸들링 구문
static class TouchTracker implements OnTouchListener {
...
private final Renderer renderer;
public TouchTracker(Renderer renderer) {
this.renderer = renderer;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
previousTouchPointPx.set(event.getX(), event.getY());
return true;
case MotionEvent.ACTION_MOVE:
float touchX = (event.getX() - previousTouchPointPx.x) / PX_PER_DEGREES;
float touchY = (event.getY() - previousTouchPointPx.y) / PX_PER_DEGREES;
previousTouchPointPx.set(event.getX(), event.getY());
float r = roll; // Copy volatile state.
float cr = (float) Math.cos(r);
float sr = (float) Math.sin(r);
accumulatedTouchOffsetDegrees.x += cr * touchX - sr * touchY;
accumulatedTouchOffsetDegrees.y += sr * touchX + cr * touchY;
accumulatedTouchOffsetDegrees.y =
Math.max(-MAX_PITCH_DEGREES,
Math.min(MAX_PITCH_DEGREES, accumulatedTouchOffsetDegrees.y));
renderer.setPitchOffset(accumulatedTouchOffsetDegrees.y);
renderer.setYawOffset(accumulatedTouchOffsetDegrees.x);
return true;
default:
return false;
}
}
제스처 상호작용을 처리하는 구문은 모두 MonoscopicView.java에 구현되어있다. 기존 제스처 핸들링 로직을 간단하게 설명하자면, OnTouchListener를 상속받는 TouchTracker 클래스 하나로 모든 제스처 이벤트를 처리되는 구조였다. 문제는 onTouchTracker 클래스 자체로는 pinch 제스처 핸들링이 불가능했으며, 두 손가락 사이의 거리를 측정하는 등 핀치 제스처에 최적화된 ScaleGestureDetector 클래스를 별도로 추가해야만 했다. 결국, 핀치줌이 실행되는 경우, 그리고 단일 터치가 이뤄지는 경우가 분기되어서 제스처가 핸들링되어야 했기때문에, onTouchListener를 통해 터치 동작이 감지될 경우 ScaleGestureDetector 그리고 단일 터치 동작만 감지하는 GestureDetector 두 클래스 중 하나만 실행될 수 있도록 다음과 같이 구문을 수정해주었다.
public void initialize() {
...
gestureDetector = new GestureDetector(getContext(), new MyGestureListener(renderer));
scaleGestureDetector = new ScaleGestureDetector(getContext(), new MyScaleListener(renderer));
setOnTouchListener(touchListener);
}
OnTouchListener touchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 터치가 감지되면, scaleGestureDetector에게 터치이벤트 우선 전달
scaleGestureDetector.onTouchEvent(event);
// scaleGestureDetector가 처리하는 터치 이벤트가 아닐 경우 gestureDetector로 이벤트 전달
if(!scaleGestureDetector.isInProgress()){
gestureDetector.onTouchEvent(event);
}
return true;
}
};
하지만 이 코드의 경우, 핀치 제스처를 하는 도중에 한 손가락을 뗄 경우 갑작스레 드래그 동작 즉, 파노라마뷰 화면각도 전환 로직으로 넘어가게 되어, 사용자 경험이 대폭 낮아지는 이슈가 있음을 확인할 수 있었다. 따라서 이 이슈를 해결하고자 isPinching이라는 전역변수를 생성하여 gestureDetector로 넘어가는 조건을 하나 더 추가해주었다.
public final class MonoscopicView extends GLSurfaceView {
// 클래스 내부에 isPinching 전역변수 추가
private boolean isPinching = false;
...
@Override
public boolean onTouch(View v, MotionEvent event) {
scaleGestureDetector.onTouchEvent(event);
if(!scaleGestureDetector.isInProgress()){
if(event.getAction() == MotionEvent.ACTION_UP){
// 두 손가락이 모두 화면을 벗어날 경우 isPinching을 false로 변경
isPinching=false;
} else{
// isPinching이 false일 경우에만 gestureDetector 실행되게 조건 추가
if(!isPinching){
gestureDetector.onTouchEvent(event);
}
}
}
return true;
};
...
private class MyScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
// ScaleGestureDetector에서 핀치동작이 감지될 경우 isPinching변수 true로 변환
public boolean onScaleBegin(ScaleGestureDetector detector) {
isPinching=true;
return true;
}
}
...
}
3. 터치 제스처로만 화면 각도 변경되게 변경
기존 video360 SampleApp 코드의 경우 드래그 제스처뿐만 아니라 기기 회전각도도 화면 각도에 영향을 주었다. 아마 핸드폰을 가상현실 HMD처럼 활용하고자 해당 로직이 구현된 것으로 보이지만, 파노라마 뷰에서는 해당 로직이 필요없으므로, 가속도센서값 Listener와 같이 위 로직과 연관된 코드들을 모두 제거해주었다.
public final class MonoscopicView extends GLSurfaceView {
// 모바일 각도 회전값 측정에 필요했던 객체들 일괄 제거
// private SensorManager sensorManager;
// private Sensor orientationSensor;
// private PhoneOrientationListener phoneOrientationListener;
public void initialize() {
// 모바일 기기 방향 Listner 및 센서 선언 구문 모두 제거
// sensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
// orientationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);
// phoneOrientationListener = new PhoneOrientationListener();
}
public void onResume() {
// onResume 호출 시 실행되는 방향 listner 등록 구문 삭제
// sensorManager.registerListener(phoneOrientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
...
}
public void onPause() {
// onPause 호출 시 실행되는 방향 listner 등록해제 구문 삭제
// sensorManager.unregisterListener(phoneOrientationListener);
}
// PhoneOrientationListener 전체 삭제
// private class PhoneOrientationListener implements SensorEventListener {
// ...
// }
// 모바일 기기의 회전값을 파노라마 뷰 각도값으로 치환하는 setRoll 함수 불요하기에 삭제
// public void setRoll(float roll) {
// We compensate for roll by rotating in the opposite direction.
// this.roll = -roll;
// }
// setDeviceOrientation 함수 삭제
// @BinderThread
// public synchronized void setDeviceOrientation(float[] matrix, float deviceRoll) {
// System.arraycopy(matrix, 0, deviceOrientationMatrix, 0, deviceOrientationMatrix.length); //**
// this.deviceRoll = -deviceRoll;
// updatePitchMatrix();
// }
'개발지식 정리 > React Native' 카테고리의 다른 글
React Native에서 PrivacyInfo.xcprivacy 대응하기 (0) | 2024.06.28 |
---|---|
React Native 스켈레톤 UI 모듈 구현 및 최적화 (0) | 2024.06.26 |
React Native 네이티브 모듈 연결하기 - 파노라마 뷰(iOS) (0) | 2024.06.20 |
React Native 네이티브 모듈 연결하기 - 파노라마 뷰(Android 2/3) (1) | 2024.06.09 |
React Native 네이티브 모듈 연결하기 - 파노라마 뷰(Android 1/3) (1) | 2024.06.06 |