펼친 상태의 대형 디스플레이와 고유한 접힌 상태는 폴더블 기기에서 새로운 사용자 환경을 제공합니다. 앱이 접힌 상태를 인식하도록 하려면 접힘 및 힌지와 같은 폴더블 기기 창 기능에 API 노출 영역을 제공하는 Jetpack WindowManager 라이브러리를 사용합니다. 앱이 접힌 상태를 인식하는 경우 접힘 또는 힌지 영역에 중요한 콘텐츠를 배치하지 않고 접힘과 힌지를 자연스러운 구분선으로 사용하도록 레이아웃을 조정할 수 있습니다.
기기가 탁자 모드나 책 모드와 같은 구성을 지원하는지 여부를 파악하면 다양한 레이아웃을 지원하거나 특정 기능을 제공하는 것에 관한 결정을 내릴 수 있습니다.
창 정보
Jetpack WindowManager의 WindowInfoTracker
인터페이스는 창 레이아웃 정보를 노출합니다. 인터페이스의 windowLayoutInfo()
메서드는 폴더블 기기의 접힌 상태를 앱에 알려주는 WindowLayoutInfo
데이터 스트림을 반환합니다. WindowInfoTracker#getOrCreate()
메서드는 WindowInfoTracker
인스턴스를 만듭니다.
WindowManager는 Kotlin Flow 및 Java 콜백을 사용하여 WindowLayoutInfo
데이터를 수집하도록 지원합니다.
Kotlin 흐름
WindowLayoutInfo
데이터 수집을 시작하고 중지하려면 재시작 가능한 수명 주기 인식 코루틴을 사용하면 됩니다. 코루틴의 repeatOnLifecycle
코드 블록은 수명 주기가 STARTED
이상이면 실행되고 수명 주기가 STOPPED
이면 중지됩니다. 수명 주기가 다시 STARTED
가 되면 코드 블록의 실행이 자동으로 다시 시작됩니다. 다음 예에서 코드 블록은 WindowLayoutInfo
데이터를 수집하여 사용합니다.
class DisplayFeaturesActivity : AppCompatActivity() {
private lateinit var binding: ActivityDisplayFeaturesBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
.windowLayoutInfo(this@DisplayFeaturesActivity)
.collect { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
}
}
}
Java 콜백
androidx.window:window-java
종속 항목에 포함된 콜백 호환성 레이어를 사용하면 Kotlin Flow를 사용하지 않고도 WindowLayoutInfo
업데이트를 수집할 수 있습니다. 아티팩트에는 WindowInfoTrackerCallbackAdapter
클래스가 포함됩니다. 이 클래스는 WindowInfoTracker
를 조정하여 WindowLayoutInfo
업데이트를 수신하는 등록 (및 등록 취소) 콜백을 지원합니다. 예를 들면 다음과 같습니다.
public class SplitLayoutActivity extends AppCompatActivity {
private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private ActivitySplitLayoutBinding binding;
private final LayoutStateChangeCallback layoutStateChangeCallback =
new LayoutStateChangeCallback();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
windowInfoTracker =
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}
@Override
protected void onStart() {
super.onStart();
windowInfoTracker.addWindowLayoutInfoListener(
this, Runnable::run, layoutStateChangeCallback);
}
@Override
protected void onStop() {
super.onStop();
windowInfoTracker
.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
SplitLayoutActivity.this.runOnUiThread( () -> {
// Use newLayoutInfo to update the layout.
});
}
}
}
RxJava 지원
이미 RxJava
(버전 2
또는 3
)를 사용 중인 경우 Observable
또는 Flowable
를 사용하여 Kotlin Flow를 사용하지 않고도 WindowLayoutInfo
업데이트를 수집할 수 있는 아티팩트를 사용할 수 있습니다.
androidx.window:window-rxjava2
및 androidx.window:window-rxjava3
종속 항목에서 제공하는 호환성 계층에는 WindowInfoTracker#windowLayoutInfoFlowable()
및 WindowInfoTracker#windowLayoutInfoObservable()
메서드가 포함되며 이러한 메서드를 통해 앱이 WindowLayoutInfo
업데이트를 수신할 수 있습니다. 예를 들면 다음과 같습니다.
class RxActivity: AppCompatActivity {
private lateinit var binding: ActivityRxBinding
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Create a new observable.
observable = WindowInfoTracker.getOrCreate(this@RxActivity)
.windowLayoutInfoObservable(this@RxActivity)
}
@Override
protected void onStart() {
super.onStart();
// Subscribe to receive WindowLayoutInfo updates.
disposable?.dispose()
disposable = observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
@Override
protected void onStop() {
super.onStop();
// Dispose of the WindowLayoutInfo observable.
disposable?.dispose()
}
}
폴더블 디스플레이 기능
Jetpack WindowManager의 WindowLayoutInfo
클래스는 디스플레이 창의 기능을 DisplayFeature
요소 목록으로 사용할 수 있도록 해 줍니다.
FoldingFeature
은 다음을 포함하여 폴더블 디스플레이에 관한 정보를 제공하는 DisplayFeature
유형입니다.
state
: 기기의 접힌 상태(FLAT
또는HALF_OPENED
)orientation
: 접힘 또는 힌지의 방향(HORIZONTAL
또는VERTICAL
)isSeparating
: 접힘 또는 힌지가 두 개의 논리 디스플레이 영역을 생성하는지 여부(true 또는 false)
HALF_OPENED
인 폴더블 기기는 화면이 두 개의 디스플레이 영역으로 분리되므로 항상 isSeparating
을 true로 보고합니다. 또한 듀얼 화면 기기에서 애플리케이션이 두 화면에 걸쳐 있는 경우 isSeparating
은 항상 true입니다.
FoldingFeature
bounds
속성 (DisplayFeature
에서 상속됨)은 접힘 또는 힌지와 같은 접기 기능의 경계 직사각형을 나타냅니다.
경계는 접기 기능을 기준으로 화면에 요소를 배치하는 데 사용할 수 있습니다.
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { ... lifecycleScope.launch(Dispatchers.Main) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { // Safely collects from WindowInfoTracker when the lifecycle is // STARTED and stops collection when the lifecycle is STOPPED. WindowInfoTracker.getOrCreate(this@MainActivity) .windowLayoutInfo(this@MainActivity) .collect { layoutInfo -> // New posture information. val foldingFeature = layoutInfo.displayFeatures .filterIsInstance<FoldingFeature>() .firstOrNull() // Use information from the foldingFeature object. } } } }
자바
private WindowInfoTrackerCallbackAdapter windowInfoTracker; private final LayoutStateChangeCallback layoutStateChangeCallback = new LayoutStateChangeCallback(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { ... windowInfoTracker = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this)); } @Override protected void onStart() { super.onStart(); windowInfoTracker.addWindowLayoutInfoListener( this, Runnable::run, layoutStateChangeCallback); } @Override protected void onStop() { super.onStop(); windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback); } class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> { @Override public void accept(WindowLayoutInfo newLayoutInfo) { // Use newLayoutInfo to update the Layout. List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures(); for (DisplayFeature feature : displayFeatures) { if (feature instanceof FoldingFeature) { // Use information from the feature object. } } } }
탁자 모드 자세
FoldingFeature
객체에 포함된 정보를 사용하여 앱은 휴대전화가 표면에 있고 힌지가 수평 위치에 있으며 폴더블 화면이 절반 정도 열려 있는 탁자 등의 상태를 지원할 수 있습니다.
탁자 모드는 사용자가 휴대전화를 손에 쥐고 있지 않은 상태에서 휴대전화를 조작할 수 있는 편리함을 제공합니다. 탁자 모드는 미디어를 보고, 사진을 찍고, 영상 통화를 할 때 적합합니다.
FoldingFeature.State
및 FoldingFeature.Orientation
를 사용하여 기기가 탁자 모드인지 확인할 수 있습니다.
Kotlin
fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL }
자바
boolean isTableTopPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL); }
기기가 탁자 모드임을 알면 그에 따라 앱 레이아웃을 업데이트합니다. 미디어 앱의 경우 일반적으로 스크롤 없이 볼 수 있는 부분에 재생을 배치하고, 핸즈프리 보기 또는 청취 환경을 위해 위치 지정 컨트롤과 보조 콘텐츠를 바로 아래에 배치하는 것을 의미합니다.
Android 15 (API 수준 35) 이상에서는 동기식 API를 호출하여 기기의 현재 상태와 관계없이 기기가 테이블탑 상태를 지원하는지 감지할 수 있습니다.
API는 기기에서 지원하는 상태 목록을 제공합니다. 목록에 탁자 상태가 포함된 경우 상태를 지원하도록 앱 레이아웃을 분할하고 앱 UI에서 탁자 및 전체 화면 레이아웃의 A/B 테스트를 실행할 수 있습니다.
Kotlin
if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) { val postures = WindowInfoTracker.getOrCreate(context).supportedPostures if (postures.contains(TABLE_TOP)) { // Device supports tabletop posture. } }
자바
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) { List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures(); if (postures.contains(SupportedPosture.TABLETOP)) { // Device supports tabletop posture. } }
예
MediaPlayerActivity
앱: Media3 Exoplayer 및 WindowManager를 사용하여 접기 인식 동영상 플레이어를 만드는 방법을 참고하세요.Jetpack WindowManager를 사용하여 폴더블 기기에서 카메라 앱 최적화 Codelab: 사진 앱에 탁자 상태를 구현하는 방법을 알아봅니다. 뷰파인더는 화면의 상단 (스크롤 없이 볼 수 있는 부분)에 표시하고 컨트롤은 화면의 하단 (스크롤해야 볼 수 있는 부분)에 표시합니다.
도서 상태
또 다른 고유한 폴더블 기능은 기기가 반쯤 열려 있고 힌지가 수직인 책 모드입니다. 책 상태는 eBook을 읽을 때 좋습니다. 책 모드는 대형 폴더블 화면에 열린 2페이지 레이아웃을 제본된 책처럼 사용하여 실제 책을 읽는 듯한 경험을 제공합니다.
핸즈프리로 사진을 찍을 때 다른 가로세로 비율을 캡처하려는 경우 사진에도 사용할 수 있습니다.
탁자 상태에 사용된 것과 동일한 기법으로 책 상태를 구현합니다. 유일한 차이점은 코드에서 접기 기능 방향이 가로가 아닌 세로인지 확인해야 한다는 것입니다.
Kotlin
fun isBookPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.VERTICAL }
자바
boolean isBookPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL); }
창 크기 변경
기기 구성이 변경되면(예: 기기가 접히거나 펼쳐지거나, 회전되거나, 멀티 윈도우 모드에서 창의 크기가 조절되는 경우) 앱의 디스플레이 영역이 변경될 수 있습니다.
Jetpack WindowManager의 WindowMetricsCalculator
클래스를 사용하면 현재 및 최대 창 측정항목을 검색할 수 있습니다. WindowManager
WindowMetrics
는 API 수준 30에서 도입된 플랫폼 WindowMetrics
와 마찬가지로 창 경계를 제공하지만 API는 API 수준 14까지 하위 호환됩니다.
창 크기 클래스 사용을 참고하세요.
추가 리소스
샘플
- Jetpack WindowManager: Jetpack WindowManager 라이브러리 사용 방법의 예
- Jetcaster : Compose를 사용한 탁자 모드 구현