Ken Burns Effect View

|

Ken Burns Effect는 패닝(Panning)과 줌(Zooming) 기반으로 사진을 화려하게 보여줄 수 있는 효과(Effect)로 사진으로부터 동영상 등을 뽑아낼 때 많이 사용하고 있습니다.

Ken Burns Effect의 모습은 다음과 같습니다.

KenBurns

이미 아주 많은 사람들이 사용하고 있는 오픈소스가 있으니 그걸 활용하도록 하겠습니다. 여기에서 소스 코드를 확인할 수 있으며, Android Studio에서는 간단히 gradle에 다음 라인만 추가하면 KenBurnsView를 사용할 수 있습니다.


Dependency 설정

dependencies {
    compile 'com.flaviofaria:kenburnsview:1.0.7'
}


Layout 코드

실제로 사용할 때는 다음과 같이 XML에 KenburnsView를 배치하기만 하면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <com.flaviofaria.kenburnsview.KenBurnsView
    android:id="@+id/image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/image" />

</RelativeLayout>


특별히 Java 코드를 따로 추가하지 않더라도 훌륭한 Ken Burns 효과를 보여주며, Java 코드를 통해 좀 더 다양하고 정교한 동작을 설정할 수 있습니다.

Apache's Http Library 충돌 해결 방법

|

Apache’s Http Library와 최신 버전의 Android SDK와 충돌이 발생하는 현상이 있습니다. 이럴 경우 다음과 같은 에러가 발생할 것입니다.

Warning:WARNING: Dependency org.apache.httpcomponents:httpclient:4.3.5 is ignored for debug as it may be conflicting with the internal version provided by Android.

그리고 빌드는 경고만 뜨고 잘 되더라도, 실행을 하려고 하면 에러가 뜨면서 실행이 안되는 현상이 생기기도 합니다. 이런 경우에는 app의 build.gradle 파일에 다음과 같은 코드를 삽입해주면 해결이 됩니다.


build.gradle

...
android {
   ...
    packagingOptions {
         exclude 'META-INF/NOTICE'
         exclude 'META-INF/LICENSE'
   }
}


전체 코드는 다음과 같습니다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        applicationId "com.datacafe.nestcamapi"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    packagingOptions {
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/LICENSE'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.0.1'
    testCompile 'junit:junit:4.12'
}

Google Awareness API 예제

|

Google Awareness API 사용 예제입니다. 일단 앞서 포스팅한 Google API Key 등록을 먼저 해야 합니다. Google API Key를 획득했다면 다음과 같이 AndroidManifest.xml 에 해당 키 정보를 기입해줍니다.


AndroidManifest.xml

<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">

  <meta-data
    android:name="com.google.android.awareness.API_KEY"
    android:value="API_KEY" />

  <activity android:name=".MainActivity">
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />

      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>
</application>


Activity_main.xml

그리고 간단한 버튼 4개를 배치해서 테스트를 하기 위한 activity_main.xml 코드는 다음과 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:orientation="vertical">

  <Button
    android:id="@+id/btn_get_location"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Get Location" />

  <Button
    android:id="@+id/btn_get_activity"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Get Activity" />

  <Button
    android:id="@+id/btn_get_place"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Get Place" />

  <Button
    android:id="@+id/btn_get_weather"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Get Weather" />

  <ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
      android:id="@+id/textview"
      android:layout_width="match_parent"
      android:layout_height="match_parent" />

  </ScrollView>
</LinearLayout>


MainActivity.java

마지막으로, MainActivity.java 코드입니다.

package com.datacafe.googleawarenessapi;

import android.content.pm.PackageManager;
import android.location.Location;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import com.google.android.gms.awareness.Awareness;
import com.google.android.gms.awareness.snapshot.DetectedActivityResult;
import com.google.android.gms.awareness.snapshot.LocationResult;
import com.google.android.gms.awareness.snapshot.PlacesResult;
import com.google.android.gms.awareness.snapshot.WeatherResult;
import com.google.android.gms.awareness.state.Weather;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.places.PlaceLikelihood;

import java.util.List;

public class MainActivity extends AppCompatActivity {

  //Activities
  //public static final int IN_VEHICLE = 0;
  //public static final int ON_BICYCLE = 1;
  //public static final int ON_FOOT = 2;
  //public static final int STILL = 3;
  //public static final int UNKNOWN = 4;
  //public static final int TILTING = 5;
  //public static final int WALKING = 7;
  //public static final int RUNNING = 8;

  private GoogleApiClient mGoogleApiClient;
  private TextView mDebugLogView;

  private StringBuilder mLogBuilder = new StringBuilder();

  private void log(String message) {
    mLogBuilder.append(message);
    mLogBuilder.append("\n");

    if(mDebugLogView != null) {
      mDebugLogView.setText(mLogBuilder.toString());
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mDebugLogView = (TextView) findViewById(R.id.textview);

    findViewById(R.id.btn_get_location).setOnClickListener(mOnClickListener);
    findViewById(R.id.btn_get_activity).setOnClickListener(mOnClickListener);
    findViewById(R.id.btn_get_place).setOnClickListener(mOnClickListener);
    findViewById(R.id.btn_get_weather).setOnClickListener(mOnClickListener);

    Log.i("snowdeer", "[snowdeer] start...");

    mGoogleApiClient = new GoogleApiClient.Builder(this)
        .addApi(Awareness.API)
        .build();
    mGoogleApiClient.connect();

    Log.i("snowdeer", "[snowdeer] GoogleApi connect..");


  }

  private View.OnClickListener mOnClickListener = new View.OnClickListener() {

    @Override
    public void onClick(View view) {
      switch(view.getId()) {
        case R.id.btn_get_location: {
          if(ContextCompat.checkSelfPermission(
              MainActivity.this,
              android.Manifest.permission.ACCESS_FINE_LOCATION) !=
              PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                MainActivity.this,
                new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                100
            );
            return;
          }

          Awareness.SnapshotApi.getLocation(mGoogleApiClient)
              .setResultCallback(new ResultCallback<LocationResult>() {
                @Override
                public void onResult(@NonNull LocationResult locationResult) {
                  if(!locationResult.getStatus().isSuccess()) {
                    log("Could not get location.");
                    return;
                  }
                  Location location = locationResult.getLocation();
                  log("Lat: " + location.getLatitude()
                      + ", Lon: " + location.getLongitude());
                }
              });
          break;
        }
        case R.id.btn_get_activity:
          Awareness.SnapshotApi.getDetectedActivity(mGoogleApiClient)
              .setResultCallback(new ResultCallback<DetectedActivityResult>() {
                @Override
                public void onResult(@NonNull DetectedActivityResult detectedActivityResult) {
                  if(!detectedActivityResult.getStatus().isSuccess()) {
                    log("Could not get the current activity.");
                    return;
                  }
                  ActivityRecognitionResult ar = detectedActivityResult
                      .getActivityRecognitionResult();
                  DetectedActivity probableActivity = ar.getMostProbableActivity();
                  log(probableActivity.toString());
                }
              });
          break;

        case R.id.btn_get_place: {
          if(ContextCompat.checkSelfPermission(
              MainActivity.this,
              android.Manifest.permission.ACCESS_FINE_LOCATION) !=
              PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                MainActivity.this,
                new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                100
            );
            return;
          }

          Awareness.SnapshotApi.getPlaces(mGoogleApiClient)
              .setResultCallback(new ResultCallback<PlacesResult>() {
                @Override
                public void onResult(@NonNull PlacesResult placesResult) {
                  if(!placesResult.getStatus().isSuccess()) {
                    log("Could not get places.");
                    return;
                  }
                  List<PlaceLikelihood> placeLikelihoodList = placesResult.getPlaceLikelihoods();

                  if(placeLikelihoodList == null) {
                    log("Result List is Null!!");
                    return;
                  }
                  // Show the top 5 possible location results.
                  for(int i = 0; i < placeLikelihoodList.size(); i++) {
                    PlaceLikelihood p = placeLikelihoodList.get(i);
                    log(p.getPlace().getName().toString()
                        + ", likelihood: " + p.getLikelihood());
                  }
                }
              });
          break;
        }

        case R.id.btn_get_weather: {
          if(ContextCompat.checkSelfPermission(
              MainActivity.this,
              android.Manifest.permission.ACCESS_FINE_LOCATION) !=
              PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                MainActivity.this,
                new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                100
            );
            return;
          }

          Awareness.SnapshotApi.getWeather(mGoogleApiClient)
              .setResultCallback(new ResultCallback<WeatherResult>() {
                @Override
                public void onResult(@NonNull WeatherResult weatherResult) {
                  if(!weatherResult.getStatus().isSuccess()) {
                    log("Could not get weather.");
                    return;
                  }
                  Weather weather = weatherResult.getWeather();
                  log("Weather: " + weather);
                }
              });

          break;
        }
      }
    }
  };
}

Google API Key 등록하기

|

Google Awareness API를 이용한 간단한 프로그램을 개발할 일이 생겼습니다. 하지만, Awareness API를 쓰기 위해서는 Google로부터 Google API Key를 등록 및 승인받아야만 사용이 가능했습니다. 그래서 여기서는 Google API Key를 등록하는 방법을 포스팅해보록 하겠습니다.


Developer Console 접속

일단 구글 개발자 콘솔에 접속한다.

image -fullwidth

여기에서 왼쪽 편의 메뉴에서 라이브러리를 선택합니다.

image -fullwidth

그리고 원하는 API를 선택합니다. 저는 Awareness API를 사용할 것이기 때문에 Awareness API를 선택했습니다. API 종류가 많기 때문에 검색을 이용하면 좀 더 편리하게 API를 찾을 수 있습니다.

image -fullwidth

화면 가운데 위쪽에 있는 사용 설정 글씨를 클릭해줍니다. 버튼 형태가 아니라서 찾기가 어려웠네요.

image -fullwidth

화면 오른편 위쪽에 사용자 인증 정보 만들기 버튼을 클릭합니다.

image -fullwidth

어떤 사용자 정보가 필요한가요 버튼을 눌러줍니다.

image -fullwidth

드디어 API 키가 생성되었다. 여기에서 키 제한하기를 눌러서 키 사용의 제한을 걸어두도록 합니다.

image -fullwidth

저는 Android 용으로 사용하기 위해서 Android 앱을 선택했습니다.

Android 앱을 선택하면 Android App의 패키지 이름과 Signed Key의 SHA1 값을 요구합니다. Signed Key의 SHA1은 화면에 있는 명령어를 커맨드 라인에서 입력하여 얻을 수 있습니다. mystore.keystore 대신 자신의 key 이름을 입력하면 됩니다.

signingConfigs{
keytool -list -v -keystore mystore.keystore

그러면 다음과 같은 출력 결과가 나오고, 이 중에서 SHA1 값만 있으면 됩니다.

image -fullwidth

App의 패키지 이름과 Signed Key의 SHA1 값을 획득하면, 위의 화면에서 패키지 이름 및 지문 추가 버튼을 눌러서 각 값을 입력해주면 됩니다.

Android Studio Gradle 에서 빌드시 Signed Key 설정하기

|

Android Studio에서 빌드할 때 Signed Key를 설정하는 방법을 포스팅하도록 하겠습니다.

물론, 본문의 내용과 곤계 없이 Android Studio에서 빌드를 할 때, Release로 메뉴에서 Generate Signed APK를 선택하여 빌드하는 방법도 있습니다. 하지만, 매번 빌드할 때마다 일일이 메뉴로 들어가서 해당 기능으로 APK를 생성하는 건 여간 번거로운 일입니다. 게다가 Release 모드가 아닌 평소 개발용인 Debug 모드로 빌드할 때도 Signed Key를 설정해야 하는 경우도 발생할 수 있기 때문에 여기서는 gradle 설정을 통해 Signed Key를 설정하는 방법을 포스팅해보겠습니다.


build.gradle 설정

app의 build.gradle 파일을 엽니다. 그리고 아래와 같이

signingConfigs{
        releaseWithSignedKey {
            storeFile file("SampleKeyStore.jks")
            storePassword "password1"
            keyAlias "sample"
            keyPassword "password2"
        }
    }

항목을 만들어 줍니다. 그리고, buildType에 아래와 같이

buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.releaseWithSignedKey
        }
    }

와 같이 설정해주면 됩니다. 그리고 만약 Debug 모드에서도 Signed Key를 참조하게 하려면 다음과 같이 하면 됩니다.

buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.releaseWithSignedKey
        }
        debug{
            signingConfig signingConfigs.releaseWithSignedKey
        }
    }


그런데, 이렇게 하면 build.gradle에 KeyStore의 민감한 정보들이 너무 노출이 되고 관리도 번거로워지기 때문에 각 값들을 변수 처리해주는 것이 깔끔합니다. 이 때는 Project의 gradle.properties에

SIGNED_STORE_FILE=SampleKeyStore.jks
SIGNED_STORE_PASSWORD=password1
SIGNED_KEY_ALIAS=sample
SIGNED_KEY_PASSWORD=password2

와 같이 작성하고, build.gradle에는

signingConfigs{
        releaseWithSignedKey {
            storeFile file(SIGNED_STORE_FILE)
            storePassword SIGNED_STORE_PASSWORD
            keyAlias SIGNED_KEY_ALIAS
            keyPassword SIGNED_KEY_PASSWORD
        }
    }

와 같이 작성을 해줍니다.


여기까지 적용한 후 Run을 해보면, Signed Key가 정상적으로 적용된 것을 알 수 있습니다.


전체 코드

참고로, 전체 build.gradle 파일 내용을 첨부하도록 하겠습니다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        applicationId "com.datacafe.googleawarenessapi"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    signingConfigs{
        releaseWithSignedKey {
            storeFile file(SIGNED_STORE_FILE)
            storePassword SIGNED_STORE_PASSWORD
            keyAlias SIGNED_KEY_ALIAS
            keyPassword SIGNED_KEY_PASSWORD
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.releaseWithSignedKey
        }
        debug{
            signingConfig signingConfigs.releaseWithSignedKey
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.0.1'


    compile 'com.google.android.gms:play-services:10.0.1'

    testCompile 'junit:junit:4.12'
}