홈스크린에 뷰 띄우기

안드로이드를 쓰다보면 홈화면에 뷰를 띄우는걸 종종 볼 수 있습니다. 대표적으로 네이버앱의 퀵메뉴나 페이스북 메신저의 챗헤드가 있죠. 퀵메뉴는 네이버앱의 각 메뉴로 바로갈 수 있는 런처 역할을, 챗헤드는 앱으로 진입하지 않고 홈화면에서 바로 대화를 이어갈 수 있도록 해줍니다. 이처럼 홈화면은 사용자가 홈버튼을 누르거나 단말을 켰을때 가장 먼저 보는 기본 화면이기에 접근성이 높아 이를 잘 이용하는 것은 매우 중요합니다.

quickmenu

오늘은 이렇게 홈스크린에 직접 뷰를 띄우는 방법에 대해 공유하려고 합니다. 이 방식은 __윈도우(Window)__를 직접 생성하고 여기에 뷰를 넣는 방식입니다. 안드로이드에서는 홈 화면을 이용할 수 있도록 위젯을 제공하지만 기본으로 제공하는 위젯은 크기와 위치가 제한됩니다. 하지만 이 방법은 위젯과는 달리 위치와 크기를 자유롭게 조절할 수 있기 때문에 활용도가 매우 높습니다.

윈도우란

아까 잠시 언급했듯이 이 방법은 직접 윈도우를 생성하고 뷰를 넣는 방식입니다. 그래서 자세한 방법을 설명하기 전에 윈도우에 대해 간단히 알고 넘어가야합니다.

윈도우는 화면에 UI를 그리기 위한 Surface를 갖습니다. 즉 화면에 뭔가 그려지고 있다는 것은 윈도우가 있는 것입니다. 또한 윈도우는 사용자와의 인터렉션을 위한 키, 터치 이벤트 등을 처리할 수 있는 컴포넌트입니다.

기본적으로 액티비티를 생성하면 액티비티는 윈도우를 갖습니다. 또한 아래 그림과 같이 토스트, 스테이터스 바, 다이얼로그등도 개별적인 윈도우를 가집니다.

토스트, 스테이터스바 다이얼로그 액티비티
ex1 ex2 ex3

윈도우 타입과 이벤트 처리

윈도우의 목적과 종류에 따라 윈도우는 서로 다른 타입을 가집니다. 예를들어 액티비티에 주로 사용되는 타입(TYPE_BASE_APPLICATION)의 윈도우는 키, 터치 이벤트를 받지만 토스트에 사용되는 타입(TYPE_TOAST)의 윈도우는 키, 터치 이벤트를 받지 못합니다. 이러한 이벤트는 윈도우의 타입에 따라 다르고 윈도우 생성 시 플래그를 통해 어느정도 제어가 가능합니다.

우선순위

윈도우의 타입에 따라 우선순위가 달라집니다. 우선순위는 화면에 그려지는 순서를 결정하는데 우선순위가 높을수록 나중에 그려져서 우선순위가 낮은 윈도우를 덮을 수 있습니다. 가령 토스트의 윈도우 타입인 TYPE_TOAST는 액티비티가 사용하는 TYPE_BASE_APPLICATION보다 우선순위가 높기때문에 액티비티에서 토스트를 띄우면 토스트가 액티비티 위에 그려지게 됩니다. 우선순위는 안드로이드 버전에 따라 조금씩 다릅니다.

구현하기

홈스크린에 뷰를 넣는 방법은 아래와 같습니다.

  1. 원하는 뷰를 생성한다.
  2. 적절한 타입과 속성을 갖는 윈도우 레이아웃 파라미터(WindowManager.LayoutParam)을 생성한다.
  3. 윈도우 매니저의 addView(View, ViewGroup.LayoutParams params)을 이용하여 윈도우를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 뷰 생성
ImageView floatingView = new ImageView(this);
floatingView.setImageResource(R.drawable.ic_pholar);

// 2. 윈도우 레이아웃 파라미터 생성 및 설정
mWindowLp = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_PHONE);
mWindowLp.width = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLp.height = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

// 3. 윈도우 생성
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mWindowManager.addView(floatingView, mWindowLp);

홈화면과 일반 액티비티는 윈도우 타입이 TYPE_BASE_APPLICATION 입니다. 반면 위 코드에서 사용한 TYPE_PHONE은 TYPE_BASE_APPLICATION이기 때문에 홈화면이나 액티비티 보다 우선순위가 높아 홈 또는 액티비티 위에 그려집니다. 또한 TYPE_PHONE는 status_bar위로 올라갈 수는 없으며 터치 이벤트를 받을 수 있습니다.

TYPE_PHONE은 아래 권한이 필요합니다.

android.permission.SYSTEM_ALERT_WINDOW

WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE는 현재 윈도우보다 하위 윈도우가 터치 이벤트를 받을 수 있도록 하는 플래그 입니다. 이걸 세팅하지 않으면 생성한 윈도우보다 낮은 순위를 갖는 홈이나 액티비티가 터치 이벤트를 받을 수 없습니다.

마지막에 보면 윈도우 매니저를 통해 윈도우를 생성하고 있습니다. 윈도우는 안드로이드의 윈도우매니저를 통해서 접근이 가능합니다. addView()를 통해 윈도우를 추가하였지만 삭제할때는 removeView()를 하면됩니다. 윈도우의 크기나 위치가 변경 되었다면 updateViewLayout()를 통해 변경사항을 적용해줍니다.

윈도우의 라이프사이클이 생성한 곳에 따라 달라집니다. 액티비티에서 생성하면 액티비티가 종료될때 사라지므로 서비스에서 생성하도록 합니다. 또한 서비스가 종료될때는 윈도우를 제거하도록 처리해주는 것도 중요합니다.

아래 이미지는 위의 코드로 생성한 홈 위젯입니다.

ex4

드래깅 구현하기

여기에 퀵메뉴나 챗헤드 처럼 자유롭게 움직일 수 있도록 하고 싶다면 아래와 같이 터치 이벤트를 처리해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
floatingView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartingX = ev.getRawX();
mStartingY = ev.getRawY();

mWidgetStartingX = mWindowLp.x;
mWidgetStartingY = mWindowLp.y;
return false;
case MotionEvent.ACTION_MOVE:
float deltaX = mStartingX - ev.getRawX();
float deltaY = mStartingY - ev.getRawY();
mWindowLp.x = (int) (mWidgetStartingX - deltaX);
mWindowLp.y = (int) (mWidgetStartingY - deltaY);
mWindowManager.updateViewLayout(floatingView, mWindowLp);
return true;
}
return false;
}
});

이벤트 처리시에 ev.getRawX(), ev.getRawY()를 사용했는데 이렇게 하면 스크린에서의 터치 위치를 가져올 수 있습니다. 이 뷰는 스크린내에서 움직여야 하기때문에 getRawX, getRawY를 사용합니다.

생성 및 제거 애니메이션 처리하기

윈도우가 생성되고 제거될때의 애니메이션은 시스템에서 제공하는 애니메이션만 적용이 가능합니다. 적용하는 방법은 아래와 같이 WindowManager.LayoutParam을 이용합니다.

mWindowLp.windowAnimations = android.R.style.Animation_Toast;

생성 및 제거 시 애니메이션만 위와 같은 제약이 있고 뷰나 레이아웃 자체에 주는 애니메이션은 자유롭게 적용이 가능합니다.

그 외 재미있는 것들

스테이터스바 높이 구하기

안드로이드 개발자 페이지에 가면 다른 튜토리얼이나 가이드는 굉장히 잘 되어있는데 이상하게 윈도우에 대한 설명은 매우 적습니다. 그래서 윈도우에 대해 제대로 이해하기가 어려웠는데요. 윈도우에 대해 앞서 설명한 정도의 배경만 알아도 많은 도움이 되는 것 같습니다. 가령 개발을 하다보면 스테이터스 바의 높이를 구하고 싶을때가 있는데요. 스택 오버플로우에서 찾아보면 아래와 같은 코드가 나옵니다.

1
2
3
4
Rect rectangle= new Rect();
Window window= getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
int statusBarHeight= rectangle.top;

처음에는 이 코드를 이해를 못하고 그냥 가져다 썼는데요. 위에 정리한 내용을 보고 다시 보면..먼저 현재 액티비티의 윈도우를 가져오고 윈도우의 Rect를 가져옵니다. 일반적인 윈도우는 스테이터스 바 아래 위치하기 때문에 액티비티 윈도우의 top이 바로 스테이터스바의 높이가 됩니다.

키보드가 올라왔는지 체크하기

또 아래 코드는 키보드가 현재 올라와 있는지를 체크하는 코드입니다. 안드로이드는 키보드가 올라와 있는지 내려가 있는지 알 수 있는 API를 제공해주지 않기 때문에 여러가지 꼼수를 써서 이를 체크해야하는데요. 가장 많이 사용되는 것이 아래의 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
contentView.getViewTreeObserver().addOnGlobalLayoutListener(new 	ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {

Rect r = new Rect();
contentView.getWindowVisibleDisplayFrame(r);
int screenHeight = contentView.getRootView().getHeight();

// r.bottom is the position above soft keypad or device button.
// if keypad is shown, the r.bottom is smaller than that before.
int keypadHeight = screenHeight - r.bottom;

Log.d(TAG, "keypadHeight = " + keypadHeight);

if (keypadHeight > screenHeight * 0.15) { // 0.15 ratio is perhaps enough to determine keypad height.
// keyboard is opened
}
else {
// keyboard is closed
}
}
});

이것도 앞서 윈도우에 대해 정리한걸 바탕으로 보면 아주 간단합니다. 현재 액티비티 윈도우의 높이를 구하고, 이 높이와 스크린의 높이를 대강 비교해서 액티비티의 높이가 키보드가 올라왔다고 판단할 수 있을만큼 작아졌다면 키보드가 올라왔다고 판단하는 것입니다.(키보드가 올라오면 액티비티의 윈도우는 그만큼 줄어듭니다)

마무리

사실 예전에 네이버 퀵메뉴를 만들때는 온전히 이해하지 못하고 적용을 했었는데 핑퐁을 하면서 정리하다보니 이제서야 오히려 더 이해를 깊게 하게 된것 같습니다. 정리하는 내내 재미있었네요. 윈도우 타입과 속성을 잘 조합하면 재미있는 것들을 많이 해볼 수 있을 것 같습니다.