2016년 9월 28일 수요일

[안드로이드] 누가(7.0) 멀티윈도우 대응

안드로이드가 7.0으로 업데이트 되면서, 갤럭시 s3 이상 휴대폰에서만 볼수 있었던 멀티윈도우기능을 공식적으로 지원하게 되었다. 이로써, 멀티윈도우 기능은 모든 안드로이드 디바이스에 탑재될 것이고, 사용자의 안드로이드 UX로 자리매김할 것이다.

멀티윈도우?

  • 한 화면에 여러 앱이 실행 가능하도록 하는 기능
  • 동시에 실행되는 앱들은 독립적으로 실행되며, 드래그&드롭으로 데이터를 주고 받을 수 있다.
  • 화면 크기가 큰 기기는 제조사가 자유형식 모드를 이용하도록 할 수 있다.
    • 자유형식 모드 : 사용자가 엑티비티의 크기를 자유롭게 조정 가능
multiwindow

멀티윈도우 구현

사전 준비

  • 모듈 하위의 build.gradle을 열고, compileSdkVersiontargetSdkVersion을 24 로 변경한다.
    • targetSdkVersion가 24보다 낮으면, compileSdkVersion이 24로 설정되어 있더라도, 시스템은 이 앱을 멀티윈도우 지원불가로 인식하고, 무조건 전체화면으로만 앱을 실행하게 된다.
    • targetSdkVersion을 24로 설정하면, compileSdkVersion에 상관없이, 시스템은 이 앱을 멀티윈도우 지원가능으로 인식한다. 단, 기본 크기로 고정되어 있어서 크기변경이 불가능하고, 어떤 부작용이 나타날지 알 수 없다.
android {
    compileSdkVersion 24
    
    defaultConfig {
        targetSdkVersion 24
        ...
    }
    ...
}

멀티윈도우 사용

  • compileSdkVersion이 24로 설정되어 있어야 한다.
  • 매니페스트 파일에 <activity> / <application> 태그에서 resizeableActivity 속성을 이용하여 멀티윈도우 기능을 활성/비활성시킬 수 있다.
android:resizeableActivity=["true" | "false"]
  • targetSdkVersion이 24이면, 이 속성을 지정하지 않았을 경우 기본값은 true이다.

Picture In Picture(PIP) 사용

  • PIP : 동영상처럼 사용자의 상호작용이 없더라도, 앱이 자동으로 계속 실행되도록 해주는 기능
  • 매니페스트 파일에 <activity> 태그에서 supportsPictureInPicture 속성을 이용하여 해당 엑티비티가 이 모드를 지원하는지 여부를 지정한다.
android:supportsPictureInPicture=["true" | "false"]
  • resizeableActivity 속성이 false인 경우, 이 속성은 무시된다.

멀티윈도우 크기 설정

  • 멀티윈도우 모드에서 앱의 크기를 변경할 수 있도록 설정하려면, / 태그 하위에 <layout> 태그를 추가하고, 다음과 같은 속성을 통해 크기를 지정해준다.
    • defaultWidth : 자유형식 모드에서 엑티비티가 시작될 때의 기본 너비
    • defaultHeight : 자유형식 모드에서 엑티비티가 시작될 때의 기본 높이
    • gravity : 자유형식 모드에서 엑티비티의 초기 배치
    • minWidth : 자유형식 / 화면분할 모드에서 엑티비티의 최소 너비
    • minHeight : 자유형식 / 화면분할 모드에서 엑티비티의 최소 높이

멀티윈도우에서의 life-cycle

  • 멀티윈도우 모드에서 Activity, Fragment의 생명주기는 변하지 않고 그대로 동작한다.
  • 현재 사용자와 상호작용중인 엑티비티가 최상단 엑티비티가 되어 활성화되고, 다른 화면의 엑티비티는 화면은 표시되지만 Pause 상태로 있다.
  • 멀티윈도우 상태에서는 Pause 상태이지만, 화면에 계속 보이는 상태이므로, 동영상 플레이어 등은 pause에서 동영상을 중지하면 안된다.

멀티윈도우 모드에서 비활성화되는 기능

  • 시스템 UI 사용자지정 옵션 비활성화
  • screenOrientation 속성 변경 무시

멀티윈도우 변경 콜백

  • 멀티윈도우 상태 변경에 따른 콜백메서드 및 상태확인 메서드가 존재한다.
  • 다음 메서드는 엑티비티, 프레그먼트 모두 추가되었다.
    • onMultiWindowModeChanged() : 현재 화면이 멀티윈도우 모드로 들어가거나, 나올때 호출되는 메서드
    • onPictureInPictureModeChanged() : 현재 화면이 PIP 모드로 들어가거나, 나올때 호출되는 메서드
    • isInMultiWindowMode() : 현재 화면이 멀티윈도우 모드인지 확인
    • isInPictureInPictureMode() : 현재 화면이 PIP 모드인지 확인 - 이 때, isInMultiWindowMode()는 무조건 true

[안드로이드] 이미지 라이브러리 - Glide

Glide란 무엇인가??

  • 구글에서 공개한 이미지 라이브러리
  • 기존의 Bump앱이 만들어 사용하던 라이브러리였는데 구글이 Bump앱을 인수하여 라이브러리를 공개
  • 웹 상의 이미지를 로드하여 보려주기 위해 고려해야 할 사항들을 미리 구현하여, 사용자가 이용하기 쉽게 만든 라이브러리

Glide 추가하기

Dependency 추가

  • build.gradle의 dependencies에 다음을 추가한다.
compile 'com.github.bumptech.glide:glide:3.7.0'
  • 혹시 maven을 이용한다면 다음을 추가한다.
<dependency>
<groupId>com.github.bumptech.glide</groupId>
<artifactId>glide</artifactId>
<version>3.7.0</version>
<type>aar</type>
</dependency>

기본 이미지 로딩

  • Glide 클래스는 빌더 패턴으로 구현되어 있고, 3개의 필수 파라미터를 요구한다.
    • with(Context context) : 안드로이드의 많은 API를 이용하기 위해 필요
    • load(String imageUrl) : 웹 상에서의 이미지 경로 URL or 안드로이드 리소스 ID or 로컬 파일 or URI
    • into(ImageView targetImageView) : 다운로드 받은 이미지를 보여줄 이미지 뷰
// 웹 URL
ImageView target = (ImageView) findViewById(R.id.imageview);
String url = "http://www.example.com/icon.png";

Glide.with(context)
.load(url)
.into(target);


// 리소스 ID
int resourceId = R.mipmap.ic_launcher;

Glide.with(context)
.load(resourceId)
.into(target);


// 로컬 파일
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Example.jpg");

Glide.with(context)
.load(file)
.into(target);


// URI
Uri uri = Uri.parse("android.resource://com.example.test/resource");

Glide.with(context)
.load(uri)
.into(target);
  • 만일 해당 경로에 이미지가 없다면, Glide는 error 콜백을 리턴할 것이다.

PlaceHolder 이미지

PlaceHolder

  • PlaceHolder : 원본이미지를 보여주기 전에 잠깐 보여주는 이미지
  • 네트워크 로드 등, 이미지 로드에 시간이 오래 걸릴 때 빈화면 대신 PlaceHolder 이미지를 보여준다.
  • Glide는 PlaceHolder 이미지를 리소스 영역에서 불러온다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.placeholder(R.mipmap.ic_launcher)
.into(target);

Error PlaceHolder

  • 이미지 로드에 실패했을 때 등, 예상하지 못한 상황으로 원본이미지를 로드할 수 없을 때 보여주는 이미지이다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.placeholder(R.mipmap.ic_launcher)
.erro(R.mipmap.ic_error) //Error상황에서 보여진다.
.into(target);

Animation

  • 원본 이미지가 다 로드되고 나면 PlaceHolder 이미지가 원본 이미지로 교체되는데, 이 때 애니메이션 처리를 할 수 있다.
  • 3.7.0 현재 버전을 기준으로, 이미지 교체 애니메이션은 기본 동작한다.
  • 애니메이션을 수동으로 On / Off 하려면 .crossFade() / dontAnimate 를 호출한다.
// Animation On
Glide.with(context)
.load("http://www.example.com/icon.png")
.placeholder(R.mipmap.ic_launcher)
.erro(R.mipmap.ic_error) //Error상황에서 보여진다.
.crossFade()
.into(target);


// Animation Off
Glide.with(context)
.load("http://www.example.com/icon.png")
.placeholder(R.mipmap.ic_launcher)
.erro(R.mipmap.ic_error) //Error상황에서 보여진다.
.dontAnimate()
.into(target);

이미지 리사이징

리사이징

  • Glide는 기본적으로 이미지뷰의 사이즈에 맞게 이미지가 다운로드되고 캐싱된다.
  • 명시적으로 이미지 사이즈를 변경하려면 override(x,y) 메서드를 호출한다.
  • 타켓 이미지뷰가 없을때, 미리 이미지를 특정 사이즈로 로드하는 용도로 사용된다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.override(600,200)
.into(target);

스케일링

  • Glide는 실제 이미지의 사이즈와 화면에 보이는 크기가 다를 때, 스케일링할 수 있는 옵션을 제공한다.

CenterCrop

  • 실제 이미지가 이미지뷰의 사이즈보다 클 때, 이미지뷰의 크기에 맞춰 이미지 중간부분을 잘라서 스케일링한다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.override(600,200)
.centerCrop()
.into(target);

fitCenter

  • 실제 이미지가 이미지뷰의 사이즈와 다를 때, 이미지와 이미지뷰의 중간을 맞춰서 이미지 크기를 스케일링한다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.override(600,200)
.fitCenter()
.into(target);

이미지 캐싱

캐싱 기본정책

  • Glide는 기본적으로 메모리 & 디스크에 이미지를 캐싱하여 불필요한 네트워크 연결을 줄인다.

메모리 캐시

  • 기본적으로 메모리 캐싱을 하기때문에, 메모리 캐싱을 위해 추가적으로 할 일은 없다.
  • 메모리 캐싱을 끄려면 skipMemoryCache(true)를 호출한다.
  • 처음 메모리 캐싱을 한 후에, skipMemoryCache(true)로 캐싱을 중지하더라도, 그 전에 저장된 캐시는 그대로 남아있다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.skipMemoryCache(true)
.into(target);

디스크 캐시

  • 기본적인 개념은 메모리 캐시와 같다. Glide는 기본적으로 디스크 캐싱을 수행한다.
  • 디스크 캐싱을 끄려면 diskCacheStrategy(DiskCacheStrategy.NONE) 메서드를 호출한다.
  • diskCacheStrategy 메서드는 DiskCacheStrategy enum을 인수로 받는다.
    • DiskCacheStrategy.NONE : 디스크 캐싱을 하지 않는다.
    • DiskCacheStrategy.SOURCE : 원본 이미지만 캐싱
    • DiskCacheStrategy.RESULT : 변형된 이미지만 캐싱
    • DiskCacheStrategy.ALL : 모든 이미지를 캐싱(기본)
  • 메모리 캐싱과는 별개이므로, 둘다 사용하지 않을 경우 다음과 같이 둘다 꺼주어야 한다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(target);

이미지 로드 우선순위

Priority

  • Glide 라이브러리는 동시에 이미지 로드 명령을 받았을 때, 지정한 우선순위에 따라 이미지를 로드하도록 지원한다.
  • priority()메서드에 Priority열거형 타입을 인수로 지정하여 우선순위를 변경한다.
    • Priority.LOW
    • Priority.NORMAL
    • Priority.HIGH
    • Priority.IMMEDIATE
  • Glide 라이브러리는 이 우선순위대로 이미지 로드를 수행하지만, 반드시 우선순위 순서대로 진행된다는 보장은 없다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.priority(Priority.HIGH)
.into(target);

썸네일

원본 썸네일

  • 원본 이미지를 썸네일로 사용한다.
  • thumbnail()메서드를 이용한다. 이 때, 크기의 배수값을 줌으로써 썸네일의 크기를 지정할 수 있다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.thumbnail(0.1f)
.into(target);
  • 원본 이미지를 사용하는 방법이기 때문에 원본이미지를 변경하면 썸네일에도 변경이 적용된다.

별도 썸네일

  • 위의 방법과는 다르게, 원본과 썸네일 이미지를 각각 로드하는 방법이다.
  • 이때도 thumbnail()메서드를 이용한다. 대신 썸네일을 위한 새로운 Request를 생성하여 인자로 주어야 한다.
// into() 메서드를 뺀 Glide Request를 생성한다.
DrawableRequestBuilder<String> thumbnailRequest = Glide
.with(context)
.load("http://www.example.com/thumbnail.png");

// 생성한 Request를 thumbnail() 메서드의 인자로 넣어준다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.thumbnail(thumbnailRequest)
.into(target);
  • 원본 이미지와는 별개의 리소스이므로 원본 이미지를 변경해도 썸네일은 변화가 없다.

Target

  • Glide는 기본적으로 이미지를 비동기로 로드하여 이미지뷰에 보여준다.
  • 하지만, Target을 이용하면 이미지뷰에 보여주는 동작이 아닌 Bitmap 자체를 얻어오는 등, 여러 동작을 수행할 수 있다.
  • Glide 입장에서는 일종의 콜백이라고 볼 수 있다.
  • Target을 구현하는 방법은 BaseTarget 추상클래스를 상속받거나, SimpleTarget을 이용한다.

SimpleTarget

  • SimpleTarget은 BaseTarget 클래스를 상속받은 클래스로 기본 동작이 구현되어 있고, onResourceReady메서드만 추가로 구현하면 된다.
  • 리소스 로드가 완료되면 onResourceReady 메서드가 호출되므로, 이 메서드 내부에서 수행할 동작을 구성하면 된다.
// 로드된 이미지를 받을 Target을 생성한다.
private SimpleTarget target = new SimpleTarget<Bitmap>() {
@Override
public void onRersourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
//TODO:: 리소스 로드가 끝난 후 수행할 작업
}
}

// 생성한 Target을 into() 메서드의 인자로 넣어준다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.asBitmap() // 리소스를 Btimap으로 강제하기 위해
.into(target);

Target을 구현할 때 고려사항

  • Target 으로 쓸 인스턴스가 가비지컬렉션의 대상이 되지 않도록 주의해야 한다. Target 인스턴스가 가비지컬렉션 되면 콜백을 받을 수 없다.
  • 엑티비티의 생명주기와는 무관한 Target일 경우, 항상 Application Context를 이용한다.

특정 크기를 지닌 Target

  • Glide는 .into()에 이미지뷰를 넘기면, 이미지뷰의 크기를 고려하여, 그 크기에 맞게 이미지를 로드한다.
  • 이와 유사하게, Target을 생성할 때, 로드될 이미지의 크기를 지정하면, Glide는 이미지를 로드할 때, 그 크기를 참조하여 이미지를 해당 크기에 맞게 로드한다.
// 로드된 이미지를 받을 Target을 생성한다. 생성할 때, 크기를 지정해준다,
private SimpleTarget target = new SimpleTarget<Bitmap>(250, 250) {
@Override
public void onRersourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
//TODO:: 리소스 로드가 끝난 후 수행할 작업
}
}

// 생성한 Target을 into() 메서드의 인자로 넣어준다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.asBitmap() // 리소스를 Btimap으로 강제하기 위해
.into(target);

RequestListener

  • Glide에서 Callback 리스너를 제공한다. 리스너는 다음의 2가지 콜백을 받는다.
    • onException : 이미지 로드 중, 예외가 생겼을 때
    • onResourceReady : 이미지 로드가 완료됬을 때
  • 각 콜백 메서드는 boolean 타입 반환인자를 가지고 있다. true일 경우, 각 콜백을 핸들링 했다는 의미이므로, Glide가 기본 후처리를 하지 않는다. 반면에, false일 경우, Glide가 기본 후처리를 진행한다.
private RequestListener<String, GlideDrawable> requestListener = new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
// 예외사항 처리
return false;
}

@Override
public boolean onResourceReady(GlideDrawable resouorce, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
// 이미지 로드 완료됬을 때 처리
return false;
}
}

// 생성한 Listener를 Glide 이미지 로드시에 추가해준다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.listener(requestListener) //리스너 추가
.into(target);

애니메이션

  • Glide는 이미지 전환시에 애니메이션을 지정할 수 있다.
  • 기본으로 제공하는 애니메이션으로는 crossFade 애니메이션이 있다.
  • 그 외에, animate 메서드를 이용해 커스텀 애니메이션을 지정할 수 있다.

xml로 애니메이션 주기

  • xml로 원하는 애니메이션을 정의한다.
  • 정의한 애니메이션 리소스를 animate 메서드의 인자로 넘겨준다.
// 애니메이션 리소스 준비 (anim.xml 이라 가정)
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-5%p" android:toXDelta="0" android duration="500" />
<alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="500" />
</set>
// 생성한 애니메이션 리소스를 이미지 로드시에 추가한다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.animate(R.anim.anim) // 리소스를 Btimap으로 강제하기 위해
.into(target);

커스텀 클래스를 이용한 애니메이션

  • ViewPropertyAnimation.Animator인터페이스를 구현한 커스텀 클래스를 만든다.
  • 만든 클래스의 인스턴스를 animate 메서드의 인자로 넘겨준다.
// 커스텀 애니메이션 클래스 준비
ViewPropertyAnimation.Animator animationObject = new ViewPropertyAnimation.Animator() {
@Override
public void animate(View view) {
// if it's a custom view class, cast it here
// then find subviews and do the animations
// here, we just use the entire view for the fade animation
view.setAlpha( 0f );

ObjectAnimator fadeAnim = ObjectAnimator.ofFloat( view, "alpha", 0f, 1f );
fadeAnim.setDuration( 2500 );
fadeAnim.start();
}
};

// 커스텀 클래스의 인스턴스를 이미지 로드시에 추가한다.
Glide.with(context)
.load("http://www.example.com/icon.png")
.animate(animationObject) // 리소스를 Btimap으로 강제하기 위해
.into(target);

참고 사이트