반응형

Guava: Google Core Libraries for Java

  • Base

 - Objects.equal() : equal 비교시 null 체크를 하지 않아도 된다.

 - Objects.hashCode() : hash코드 생성을 보다 쉽게 만들 수 있다.

 - Objects.toStringHelper() : toString객체를 보다 쉽게 만들 수 잇다.

  -> 일반적인 코드 :

public class Book {
private String title;
private String writer;
private int price;

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getWriter() {
return writer;
}

public void setWriter(String writer) {
this.writer = writer;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Book book = (Book) o;

if (price != book.price) return false;
if (title != null ? !title.equals(book.title) : book.title != null) return false;
return !(writer != null ? !writer.equals(book.writer) : book.writer != null);

}

@Override
public int hashCode() {
int result = title != null ? title.hashCode() : 0;
result = 31 * result + (writer != null ? writer.hashCode() : 0);
result = 31 * result + price;
return result;
}

@Override
public String toString() {
return "Book{" +
"title='" + title + '\'' +
", writer='" + writer + '\'' +
", price=" + price +
'}';
}
}

  -> Guava를 이용한 코드

public class Book {
private String title;
private String writer;
private int price;

public Book(String title, String writer, int price) {
this.title = title;
this.writer = writer;
this.price = price;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
Preconditions.checkNotNull(title);
this.title = title;
}

public String getWriter() {
return writer;
}

public void setWriter(String writer) {
this.writer = writer;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equal(this.title, book.title) && Objects.equal(this.writer, book.writer)
&& this.price == book.price;

}

@Override
public int hashCode() {
return Objects.hashCode(this.title, this.writer, this.price);
}

@Override
public String toString() {
return Objects.toStringHelper(this)
.add("title", title)
.add("writer", writer)
.add("price", price)
.toString();
}
}

작성하다보니 Objects.toStringHelper 가 deprecated가 되었네요.. 이건 MoreObject.toStringHelper()를 이용하면 됩니다.

 - Preconditions : 객체 정보의 null과 같은 정보가 있는지를 미리 확인하여 exception을 낸다.

  -> 일반적인 코드

public void setTitle(String title) {

if(title == null) {
throw new NullPointerException("title must not be null!");
}

this.title = title;
}

  -> Preconditions를 이용

public void setTitle(String title) {
Preconditions.checkNotNull(title);
this.title = title;
}

 - Joiner : Collections객체들이나 array객체들의 정보들을 string으로 변경해준다.

List names = new ArrayList<>();
names.add("test1");
names.add("test2");
names.add("test3");

Joiner.on(", ").join(names); // test1, test2, test3

 - Splitter : Joiner와 반대로 split 캐릭터를 기준으로 Iterator객체로 변환하여 준다.

Iterator names = Splitter.on(",")
.trimResults()
.omitEmptyStrings()
.split("test1, test2, test3").iterator();

names.next(); //test1
names.next(); //test2
names.next(); //test3

  • Collections
 - Collections객체를 생각하면 기본적으로 List, Set, Map등을 생각합니다. 그리고 이런 Collection 객체들을 생성할 때 new 를 통해 객체 생성을 하죠.
기본적으로 List 를 본다면 new ArrayList<Strig>();와 같이 합니다. 하지만 Guava에서는 static factory 생성자를 이용합니다.

Lists.newArrayList();
Sets.newHashSet();
Maps.newHashMap();

 - Immutable Collections : 변경 불가능한 Collection.. 객체들에 대한 의도하지 않은 변경을 막고 Thread-Safe 한 객체를 만들 수 있다.

Set<String> nameSet = Collections.unmodifiableSet(new LinkedHashSet<String>(Arrays.asList("test1", "test2", "test3")));
Set<String> nameSet = ImmutableSet.of("test1", "test2", "test3");

이와 같이 변경불가능 객체를 만들 때 기존 코드보다 간결하게 만들 수 있다. Set 이와에 Map에 대해서도 알아보면.

Map<String, Integer> mapValue1 = new LinkedHashMap<>();
mapValue1.put("test1", 1);
mapValue1.put("test2", 2);
mapValue1.put("test3", 3);
Map<String, Integer> unModifiedMap = Collections.unmodifiableMap(mapValue1);

Map<String, Integer> mapValue2 = ImmutableMap.<String, Integer>builder()
.put("test1", 1).put("test2", 2).put("test3", 3)
.build();

Map<String, Integer> mapValue3 = ImmutableMap.of("test1", 1, "test2", 2, "test3", 3);

이와 Immutable Collection이 있는 것들은
  • ImmutableCollection
  • ImmutableList
  • ImmutableSet
  • ImmutableSortedSet
  • ImmutableMap
  • ImmutableSortedMap
  • ImmutableMultiset
  • ImmutableSortedMultiset
  • ImmutableMultimap
  • ImmutableListMultimap
  • ImmutableSetMultimap
  • ImmutableBiMap
  • ImmutableClassToInstanceMap
  • ImmutableTable
이렇게 존재한다. 기존 java 코드보다는 훨씬 간결한 코드를 이용할 수 있다.
 - BiMap : key에 대한 unique한 값을 보장하는게 아니라 value에 대한 unique 성을 보장한다.
 - MultiSet : element의 중복을 허용한다.
 - MultiMap : Map이 Key의 중복을 허용하지 않지만,  MultiMap은 Key의 중복을 허용한다. MultiMap은 Map<K, Collection<V>>의 특성을 가지고 있다.

  • Functional programming

public interface Function {
@Nullable T apply(@Nullable F input);
}

이와 같이 F를 입력 받아서 T의 결과를 리턴한다.
예를 든다면 마일을 입력받아서 킬로미터로 출력한다.

Function<Integer, Double> calcMileToKillometer = new Function<Integer, Double>() {
@Override
public Double apply(@Nullable Integer input) {
return input * 1.60934;
}
};

double miles = calcMileToKillometer.apply(1);

- filter / transform
 객체에 있는 특정 값들이 속한 객체들만 골라낸 Collection을 만들거나, 특정 값들만 뽑아낸 새로운 Collection을 만들어 낼 수 있다.

List<Book> bookList = ImmutableList.of(new Book("Android", "test", 100), new Book("Java", "test2", 200), new Book("Object", "test3", 300));
List<Book> androidBookList = ImmutableList.copyOf(Collections2.filter(bookList, new Predicate<Book>() {
@Override
public boolean apply(@Nullable Book input) {
return "Android".equals(input.getTitle());
}
}));

List<String> titleList = ImmutableList.copyOf(Collections2.transform(bookList, new Function<Book, String>() {
@Override
public String apply(@Nullable Book input) {
return input.getTitle();
}
}));


우선 제가 자주 사용하는 것들로만 정리..
내부적으로 아직 @Beta annotation이 된 것들도 많아서.. 아직 그것들에 대해서는 적어두진 않았습니다. 


반응형
반응형


안드로이드 개발하는 개발 패턴으로는 MVC( Model - View, Controller)와, MVVM(Model - View, ViewModel), 그리고 MVP(Model - View - Presenter)방법이 있습니다. 이 세가지 방법에는 각자만의 스타일과 장단점을 가지고 있구요. 안드로이드 개발자들이 많이 늘어나고 좀더 편한 개발 방법들을 추가하면서 나오는 방법들이지 않을 까 합니다.

1. MVC ( Model - View - Controller )

 - Model : 안드로이드에서 사용될 데이터들.

 -  View : activity_main.xml 과 같은 view layout

 - Controller : Activity, Fragment 들...

알게 모르게 가장 많이 사용된 패턴이지 않을 까 생각됩니다. 그런데 View와 Controller의 coupling이 너무 강합니다. 누구 하나 땔래야 땔 수 없는 구조이죠. 우리가 알고 있는 좋은 구조는 loose coupling이 많아야 좋은 구조라고 배웠을 겁니다. 그래서 장기적으로 봤을 대 어떤 구조로 가야 좋을 지 생각해 볼 필요가 있습니다.


2. MVVM ( Model - View - ViewModel )

ViewModel은 Controller와 View의 구조를 통합적으로 가지고 있다고 보시면 될 거 같습니다. ViewModel을 독립적으로 가져갈 수 있지만, 나중에 View가 변경된다면 ViewModel을 한번에 다 변경해야 할 필요도 있어 보입니다. 가끔 Git에서 안드로이드 ui 라이브러리나 componet를 보면 mvvm으로 구현된 것들이 많았던 것으로 기억됩니다. 이 방법이 그나마 View에 대한 가장 독립적인 방법이지 않았을 까 생각됩니다. 


3. MVP ( Model - View - Presenter )

전체적인 패턴이 MVC와 비슷합니다. Controller가 Presenter로 보시면 될 듯 하지만 틀린 점이라면 View에 대한 관리와 접근은 Activity에서 하고, Model에 대한 컨트롤러 역활은 Presenter에서 하게 됩니다. 


이 세 방법에 대한 생각은 제 주관적인 생각입니다. MVC, MVVM, MVP 뭐 하나 나쁘다 좋다 할 방법은 없다고 생각 됩니다.  각자만의 장단점이 있고 누가 어떻게 사용하느냐 그리고 어떤 프로젝트 인지에 따라서도 많이 틀려질 것으로 보입니다. 저는 최근에 프로젝트를 MVP방법으로 진행했습니다. MVP를 사용하면서 경험을 해봤을 대 각각 구조에 대해서 loose coupling적인 방법으로 구현을 할 수 있었지만, 너무 독립적으로 진행하다보니 해당 interface에 대한 구현이 많아지는 것을 느낄 수 있었습니다. 다른 프로젝트를 진행한다면 다시 MVP방법을 사용해 볼려고는 합니다. 단지 저는 MVC, MVVM방법보다는 그래도 가장 느슨한 결합도를 가졌다고 보여집니다.

공부하면서 만든 샘플은 git에 올려 둡니다. 그냥 참고 자료에요.

https://github.com/drcarter/AndroidDevPattern

반응형
반응형

Dagger2

url : http://google.github.io/dagger/

요즘 안드로이드 개발에서 Dependency Injection방법을 이용한 개발 방법이 이슈로 자리잡고 있는 듯 합니다. 더군다나 네임드 개발자로 알려진 Jake Wharton 형님이 발표한 자료도 있습니다.

https://speakerdeck.com/jakewharton/dependency-injection-with-dagger-2-devoxx-2014

2라는 숫자가 있는 것을 보면 Dagger1이 있다는 얘기가 됩니다. 처음 Dagger는 Square에서 나왔습니다. 

http://square.github.io/dagger/

이 사이트에 가시면 자세한 설명을 보실 수 있어요. 나중에 Dagger에 대한 샘플을 작성해 볼 생각입니다.


Dagger2를 얘기한 이유는 프로젝트가 google github에 있다는 것 때문입니다. 믿고 써보는 google??이라서?


Dagger2에서 중요한 Annotation은  @Module, @Provides, @Component, @Inject 정도 일 것 같습니다.

@Module : Module로 구성되고, 내부에서 주입될 객체들을 provide해주는 것.

@Provides : 객체 주입에 필요한 내용을 리턴해주는것

@Component : Module과 Inject간의 Bridge같은 역활.

@Inject : 객체의 주입.

대충 이런정도의 설명... 저는 참 설명을 못하네요 ㅡ.ㅡ


Dagger2를 이용하기 위한 전반적인 순서 입니다.

gradle 스크립트에 아래와 같은 내용을 추가 해 줍니다.

apply plugin: 'com.neenbedankt.android-apt'

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
    }
}

android {
    ........

    dependencies {
        .......
        provided 'javax.annotation:jsr250-api:1.0'
        apt 'com.google.dagger:dagger-compiler:2.0'
        compile 'com.google.dagger:dagger:2.0'
    }
}


그리고 사용할 모델들을 정의 해 줍니다. 저는 아래와 같은 예제를 만들어 보았습니다.

Motor.java

public class Motor {

    private int speed;

    public Motor() {
        this.speed = 0;
    }

    public int getSpeed() {
        return speed;
    }

    public void accelerate(int value) {
        speed = speed + value;
    }

    public void setSpeed(int value) {
        speed = value;
    }

    public void brake() {
        speed = 0;
    }
}


Bike.java

public class Bike {

    private Motor motor;

    public Bike(@NonNull Motor motor) {
        this.motor = motor;
    }

    public void increaseSpped() {
        this.motor.setSpeed(this.motor.getSpeed() + 1);
    }

    public void decreaseSpeed() {
        if (this.motor.getSpeed() > 0) {
            this.motor.setSpeed(this.motor.getSpeed() - 1);
        }
    }

    public int getSpeed() {
        return this.motor.getSpeed();
    }
}


그리고 Module을 정의 합니다.

우선 MotorModule.java 입니다. 이건 단순히 Motor객체를  제공해 주는 역활만 합니다.

@Module
public class MotorModule {

    @Provides
    Motor provideMotor() {
        return new Motor();
    }
}


BikeModule.java입니다.

@Module(
        includes = MotorModule.class
)
public class BikeModule {

    @Provides
    Bike provideBike(Motor motor) {
        return new Bike(motor);
    }
}


여기서 보면 Bike객체를 제공해주는 @Provides을 보면 Motor객체가 파라미터로 존재합니다. 과연 이 파라미터는 어디서 제공받고 있을까 라는 의문이 듭니다. 이건 @Module의 includes부분에 보면 MotorModule.class를 포함하고 있고, MotorModule에서 Motor객체를 provides하고 있기에 가능합니다.

이렇게 되면 BikeModule안에서 Motor객체를 필요로 하는 곳에서는 모두 파라미터로 정의하면 Motor객체를 제공받을 수 있습니다.

그리고 Provides된 것을 Inject 하여 사용할 수 있는데 Bike객체를 Inject하여 사용해 볼까 합니다.

그럼 Bike는 어디에서 사용할 까요? 위에서 중요 Annotation중에 @Component를 알려드렸습니다. Module과 Inject간의 Brige같은 역활을 한다고 했는데 사용할 Component는 아래와 같습니다.

MainActivityComponent.java

@Component(
        ...
        modules = {
                ...
                BikeModule.class
        }
)
public interface MainActivityComponent extends ActivityComponent {
    void inject(@NonNull MainActivity mainActivity);
}


이와 같이 Component를 정의했습니다.

이렇게 Component를 정의하고 컴파일 하면 "DaggerMainActivityComponent가" 만들어 집니다. 이걸 가지고 실질적으로 객체 주입을 진행할 수 있죠.

DaggerMainActivityComponent.builder()
        ...
        .bikeModule(new BikeModule())
        .build();


이와 같이 하면 사용할 Component에 대해서는 준비가 끝납니다. 그럼 객체 주입은 어떻게 할까요?  몇가지 방법이 있지만 저는 객체에 @Inject anootation방법을 이용해서 사용 할려고 준비했습니다.


@Inject
Bike bike;

이와 같이 하여 Bike객체 사용에 대한 준비를 합니다.

이건 component에서 inejct라는 메소드를 정의해서 상용할 수 있는 부분입니다 그럼  inject라는 것을 하지 않고 사용할 방법은 있을까요? Component에서 getBike()라는 메소드를 만들어서 사용할 방법도 있긴 합니다.

이 예제에 대한 소스는

https://github.com/drcarter/Dagger2Example

에 올려둿습니다..

저도 Dagger2에 대해서 공부하고 정리하다 보니 설명이 명확하진 않은 듯 합니다.

오히려 맨 위에 Jake Wharton형님이 pt로 설명한 내용이 더 좋을지도요 ㅎㅎㅎ


반응형
반응형

안드로이드에서 onActivityResult는 이전 activity에서의 결과를 intent를 통해서 받을 수 있습니다. 개발문서 참고

당연히 Fragment안에서도 onActivityResult가 있고, fragment안에서 startActivityForResult를 통해서 해당 결과를 받을 수 있습니다.


/**
     * Call {@link Activity#startActivityForResult(Intent, int)} from the fragment's
     * containing Activity.
     */
    public void startActivityForResult(Intent intent, int requestCode) {
        if (mHost == null) {
            throw new IllegalStateException("Fragment " + this + " not attached to Activity");
        }
        mHost.onStartActivityFromFragment(this /*fragment*/, intent, requestCode);
    }

이와 같이 fragment에서 startActivityForResult를 하기 위해선 fragment가 activity에 attach되어 있어야 합니다.

fragment에서 startActivityForResult를 Activity에서 보낸 것처럼 할려면

getActivity().startActivityForResult(.......)

로 해서 보내면 됩니다. 이렇게 해서 보내면 onActivityResult로 받게 되는 결과가 fragment에서 바로 받는 것이 아니라 activity에 있는 onActivityResult에서 받게 되어서 이 결과를 바로 fragment에 전달해 줘야 하죠.... 이 부분이 조금 껄끄러운 면이 있습니다. Lollipop에서 material design을 적용하여 Activity Transition이나 Fragment Transition을 적용할려면, onStartActivityForResult의 호출지점이 fragment가 아닌 activity에서 하는 것처럼 보내야 합니다.

그럼 Activity의 onActivityResult로 받은 결과를 어떻게 fragment에 전달하는 것이 좋을까요?

여러가지 방법이 있을 듯 합니다. 가장 쉬운 방법으로는 Activity에서 해당 fragment의 instance를 두고서 전달할 public method를 만들어 전달 할 방법이 있을 것 같습니다. 

제가 이번에 소개해볼 방법은 event bus[Otto - An event bus by Square]를 이용한 방법을 소개해 볼까 합니다. 

ActivityResultEvent.java 로 만든 class 입니다.


public class ActivityResultEvent {

    private int requestCode;
    private int resultCode;
    private Intent data;

    public static ActivityResultEvent create(int requestCode, int resultCode, Intent intent) {
        return new ActivityResultEvent(requestCode, resultCode, intent);
    }

    private ActivityResultEvent(int requestCode, int resultCode, Intent data) {
        this.requestCode = requestCode;
        this.resultCode = resultCode;
        this.data = data;
    }

    public int getRequestCode() {
        return requestCode;
    }

    public int getResultCode() {
        return resultCode;
    }

    public Intent getData() {
        return data;
    }
}

이건 단순히 onActivityResult로 받는 requestCode, resultCode, intent정보를 가지고 fragment에 전달해 주는 용도 입니다.

EventBus.java


public class EventBus extends Bus {

    private static EventBus instance;

    public static EventBus getInstance() {
        if (instance == null) {
            synchronized (EventBus.class) {
                if (instance == null) {
                    instance = new EventBus();
                }
            }
        }
        return instance;
    }

    private Handler handler = new Handler(Looper.getMainLooper());

    @Override
    public void post(final Object event) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            super.post(event);
        } else {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    EventBus.super.post(event);
                }
            });
        }
    }
}

이건 ActivityResultEvent를 보내줄 Bus입니다.. 이 Bus의 이용 방법은 위에 링크 걸린 홈페이지에 자세히 설명 되어 있어요 

해당 EventBus를 Singleton으로 만들었습니다. 


이 부분의 사용 방법은

activity의 onActivityResult에서


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//        super.onActivityResult(requestCode, resultCode, data);
        EventBus.getInstance().post(ActivityResultEvent.create(requestCode, resultCode, data));
    }

이와 같이 호출하면 되고,

fragment에서는 

    @SuppressWarnings("unused")
    @Subscribe
    public void onActivityResultEvent(@NonNull ActivityResultEvent event) {
        onActivityResult(event.getRequestCode(), event.getResultCode(), event.getData());
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
//        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case Constants.RequestCode.REQUEST_CODE_NAME_INPUT: {
                if (resultCode == Activity.RESULT_OK) {
                    String text = data.getStringExtra(KeySets.KEY_NAME_INPUT);
                    if (!TextUtils.isEmpty(text)) {
                        textResult.setText(text);
                    }
                }
                break;
            }
        }
    }

이와 같이 Bus를 통해 받은 정보를 fragment의 onActivityResult로 전달하면 됩니다.

Bus를 통해서 정보 공유는 이요할 곳이 참 많아 보입니다. Service에서 activity나 fragment에 결과의 내용을 전달 할 때 사용해도 되구요.

결과를 만들어 내는 방법에는 여러 방법이 있지만, 사용하는건 개발자의 선택인 듯 합니다. 방법에는 정답이 없으니까요.

source : https://github.com/drcarter/RetriveActivityResult

샘플 소스는 제 git에 올려둡니다.

반응형
반응형



hexagon이미지를 만드는 방법입니다.

방법은 간단히.. 이미지의 중심점을 잡고, 반지름을 구한 뒤 중심점에서 각 0도, 60도, 120도, 180도, 240도, 360도 위치의 점을 잇는 선을 그은 뒤 그에 해당하는 이미지만 뽑아내느 방법입니다.

이걸 활용하면 android에서 사용할 HexagonImageView도 만들어 볼 수 있겠죠.

코드는 아래와 같습니다.


private Bitmap getHexagonImage(Bitmap toTransform) {
    Bitmap output = Bitmap.createBitmap(toTransform.getWidth(),
            toTransform.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(output);

    int radius = 0;
    int width = toTransform.getWidth();
    int height = toTransform.getHeight();
    int centerX = width / 2;
    int centerY = height / 2;

    if (width >= height) {
        radius = height / 2;
    } else {
        radius = width / 2;
    }

    Paint paint = new Paint();
    final Rect rect = new Rect(0, 0, toTransform.getWidth(),
            toTransform.getHeight());

    Path path = new Path();
    for (int i = 0; i < 6; i++) {

        int posX = (int) (centerX + radius * Math.cos(Math.toRadians(i * 60 - 30)));
        int posY = (int) (centerY + radius * Math.sin(Math.toRadians(i * 60 - 30)));
        if (i == 0) {
            path.moveTo(posX, posY);
        } else {
            path.lineTo(posX, posY);
        }
    }

    path.close();
    canvas.drawARGB(0, 0, 0, 0);
    paint.setColor(Color.parseColor("#FFFFFF"));
    canvas.drawPath(path, paint);
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    canvas.drawBitmap(toTransform, rect, rect, paint);

    return output;
}


반응형
반응형

1. SpannableString

2. setPaintFlags

3. Html.fromHtml


SpannableString

String udata="SpannableString underline sample";
SpannableString content = new SpannableString(udata);
content.setSpan(new UnderlineSpan(), 0, udata.length(), 0);
mTextView.setText(content);



setPaintFlags

mTextView.setPaintFlags(mTextView.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
mTextView.setText("Paint flag underline sample");



Html.fromHtml

String htmlString="Html from underline sample";
mTextView.setText(Html.fromHtml(htmlString));


반응형
반응형

Android 개발할 때 eclipse를 통하여 개발을 많이 했는데, AndroidStudio를 통하여 개발을 해보게 되었습니다.

그중에 Build상태에서는 오류가 나오진 않았지만 Runtime시에 

Excution failed for task ':app:preDexDebug'

이와 같은 오류를 보게 되었습니다. Build는 되었는데 왜 Runtime시에 나타날까에 대한 것도 찾아 보았을 때 추가해준 Library중 하나가 AndroidLibrary가 아닌 Java LIbrary로 된 것이 있었습니다. 그리고 개발중에 사용된 java version이 

Java SE 8u25

버전을 사용했더니 문제가 나타났습니다. gradle빌드에 1.6이라는 옵션을 주었지만 해결되지 않았죠 ㅡㅡ.

혹시나 하는 마음에 

Java SE 7u71/72 

를 설치하고 JDK location을 변경해 주었더니 문제가 없어졌습니다.... 아무래도 java8에서 빌드된 java library가 dex로 변경을 할 수 없는 듯 해 보입니다. 이런식으로 해결은 했지만 그래도 좀 찜찜하네요 ㅡㅡ.



반응형
반응형

안드로이드에서 음악을 재생할 때 가장 많이 사용하는 것으 MediaPlayer 입니다. 사용 방법도 간단하고 Added in API level 1 이 알려주다 싶이 Media 재생을 위한 api로는 가장 오래되고 많이 사용을 하죠. 그럼 Wave 파일에 대해서는 어떻게 재생을 할까요??? AudioTrack이라는 api가 존재합니다. pcm 데이터를 재생시켜줄 수 있는 api인데, 사실 mp3나 aac 같은 파일을 재생하기 위해서 Decoder로 mp3나 aac같은 파일을 다시 pcm데이터인 wave 파일로 만들어야 AudioTrack으로 재생시킬 수 있습니다. 

그럼 mp3나 aac와 같은 파일을 wave로의 Decoder는 api로 존재를 하느냐?.. API level 16인 Jelly Bean(Android 4.1) 부터 AudioCodec 이라는 api가 새로 생겼습니다. AudioCodec은 Encoder와 Decoder 기능을 같이 제공 합니다. 그래서 오디오 정보는 AudioTrack으로 재생을 할 수 있는 것이죠.

생성 방법은

MediaCodec codec = MediaCodec.createDecoderByType(type);
MediaCodec codec = MediaCodec.createEncoderByType(type);

이와 같은 방법으로 MediaCodec을 생성해서 사용할 수 있습니다.



디코딩을 통한 재생하는 메인 코드 입니다. 원리는 해당 버퍼만큼 디코딩하고 그에 해당하는 부분으 AudioTrack을 통한 write로 재생을 하는 방법입니다.
private void decodeLoop()
	{
		ByteBuffer[] codecInputBuffers;
		ByteBuffer[] codecOutputBuffers;

		mExtractor = new MediaExtractor();
		try
		{
			mExtractor.setDataSource(this.mMediaPath);
		}
		catch (Exception e)
		{
			mAudioPlayerHandler.onAudioPlayerError(AudioStreamPlayer.this);
			return;
		}

		MediaFormat format = mExtractor.getTrackFormat(0);
		String mime = format.getString(MediaFormat.KEY_MIME);
		long duration = format.getLong(MediaFormat.KEY_DURATION);
		int totalSec = (int) (duration / 1000 / 1000);
		int min = totalSec / 60;
		int sec = totalSec % 60;

		mAudioPlayerHandler.onAudioPlayerDuration(totalSec);

		Log.d(TAG, "Time = " + min + " : " + sec);
		Log.d(TAG, "Duration = " + duration);

		mMediaCodec = MediaCodec.createDecoderByType(mime);
		mMediaCodec.configure(format, null, null, 0);
		mMediaCodec.start();
		codecInputBuffers = mMediaCodec.getInputBuffers();
		codecOutputBuffers = mMediaCodec.getOutputBuffers();

		int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);

		Log.i(TAG, "mime " + mime);
		Log.i(TAG, "sampleRate " + sampleRate);

		mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, AudioFormat.CHANNEL_OUT_STEREO,
				AudioFormat.ENCODING_PCM_16BIT, AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO,
						AudioFormat.ENCODING_PCM_16BIT), AudioTrack.MODE_STREAM);

		mAudioTrack.play();
		mExtractor.selectTrack(0);

		final long kTimeOutUs = 10000;
		MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
		boolean sawInputEOS = false;
		int noOutputCounter = 0;
		int noOutputCounterLimit = 50;

		while (!sawInputEOS && noOutputCounter < noOutputCounterLimit && !isForceStop)
		{
			if (!sawInputEOS)
			{
				if(isPause)
				{
					if(mState != State.Pause)
					{
						mState = State.Pause;
						
						mAudioPlayerHandler.onAudioPlayerPause();
					}
					continue;
				}
				noOutputCounter++;
				if (isSeek)
				{
					mExtractor.seekTo(seekTime * 1000 * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
					isSeek = false;
				}

				mInputBufIndex = mMediaCodec.dequeueInputBuffer(kTimeOutUs);
				if (mInputBufIndex >= 0)
				{
					ByteBuffer dstBuf = codecInputBuffers[mInputBufIndex];

					int sampleSize = mExtractor.readSampleData(dstBuf, 0);

					long presentationTimeUs = 0;

					if (sampleSize < 0)
					{
						Log.d(TAG, "saw input EOS.");
						sawInputEOS = true;
						sampleSize = 0;
					}
					else
					{
						presentationTimeUs = mExtractor.getSampleTime();

						Log.d(TAG, "presentaionTime = " + (int) (presentationTimeUs / 1000 / 1000));

						mAudioPlayerHandler.onAudioPlayerCurrentTime((int) (presentationTimeUs / 1000 / 1000));
					}

					mMediaCodec.queueInputBuffer(mInputBufIndex, 0, sampleSize, presentationTimeUs,
							sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);

					if (!sawInputEOS)
					{
						mExtractor.advance();
					}
				}
				else
				{
					Log.e(TAG, "inputBufIndex " + mInputBufIndex);
				}
			}

			int res = mMediaCodec.dequeueOutputBuffer(info, kTimeOutUs);

			if (res >= 0)
			{
				if (info.size > 0)
				{
					noOutputCounter = 0;
				}

				int outputBufIndex = res;
				ByteBuffer buf = codecOutputBuffers[outputBufIndex];

				final byte[] chunk = new byte[info.size];
				buf.get(chunk);
				buf.clear();
				if (chunk.length > 0)
				{
					mAudioTrack.write(chunk, 0, chunk.length);
					if (this.mState != State.Playing)
					{
						mAudioPlayerHandler.onAudioPlayerPlayerStart(AudioStreamPlayer.this);
					}
					this.mState = State.Playing;
				}
				mMediaCodec.releaseOutputBuffer(outputBufIndex, false);
			}
			else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED)
			{
				codecOutputBuffers = mMediaCodec.getOutputBuffers();

				Log.d(TAG, "output buffers have changed.");
			}
			else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)
			{
				MediaFormat oformat = mMediaCodec.getOutputFormat();

				Log.d(TAG, "output format has changed to " + oformat);
			}
			else
			{
				Log.d(TAG, "dequeueOutputBuffer returned " + res);
			}
		}

		Log.d(TAG, "stopping...");

		releaseResources(true);

		this.mState = State.Stopped;
		isForceStop = true;

		if (noOutputCounter >= noOutputCounterLimit)
		{
			mAudioPlayerHandler.onAudioPlayerError(AudioStreamPlayer.this);
		}
		else
		{
			mAudioPlayerHandler.onAudioPlayerStop(AudioStreamPlayer.this);
		}
	}


사실 이 방법을 사용하는 것은 기존의 MediaPlayer를 사용해서 재생하는 방법보다는 상당히 복잡합니다.

하지만 이 방법을 사용하면 바로 pcm데이터를 통한 가공을 할 수 있다는 장점이 있습니다. MediaPlayer에서도 Visualizer를 통하여 pcm데이터를 가져올 수 있는 방법도 있습니다. 

설명보다는... 그냥 코드로~

https://github.com/drcarter/AudioStreamPlayer


반응형

+ Recent posts