Added another resource directory in Gradle

Adding new features to the existing project required extra drawable and layout resources, which made me interested in creating another resource directory. I knew that I could separate the resource directory with Gradle, but I didn’t try it before.

What I want to do is to add another directory for new resources like this:

app
    src
        main
            res
                drawable
                layout
                ...
            res2
                drawable
                layout
                ...

I added below to build.gradle like in developer site (https://developer.android.com/studio/write/add-resources.html),

sourceSets {
    main {
        res.srcDirs = ['res', 'res2']
    }
}

However, NOT WORKS! Resource files are not processed!

I guessed what the problem was, I changed like below. IT WORKS.

sourceSets {
    main {
        res.srcDirs = ['src/main/res', 'src/main/res2']
    }
}
Added another resource directory in Gradle

그레이들에서 리소스 디렉토리를 추가해 보았다

기존 프로젝트에 새로운 기능을 추가하면서 drawable과 layout 리소스를 추가해야 했는데, 갑자기 호기심이 생겨서 리소스 디렉토리를 하나 더 만들어서 추가해 보았다. 그레이들을 사용하면 리소스 디렉토리를 분리할 수 있다는 사실은 알았는데 적용해 보지 못하다가 처음 시도해 봤다.

만들려는 구조는 이런 식으로 리소스를 위한 하나의 큰 디렉토리를 추가하려는건데,

app
    src
        main
            res
                drawable
                layout
                ...
            res2
                drawable
                layout
                ...

개발자 페이지(https://developer.android.com/studio/write/add-resources.html)에 있는 것처럼 아래와 같이 build.gradle에 추가하니,

sourceSets {
    main {
        res.srcDirs = ['res', 'res2']
    }
}

안된다! 리소스 파일이 제대로 인식되지 않는다!

그래서 뭐가 문제인지 생각해보다 혹시나 하고 아래와 같이 바꿔보니, 동작한다

sourceSets {
    main {
        res.srcDirs = ['src/main/res', 'src/main/res2']
    }
}
그레이들에서 리소스 디렉토리를 추가해 보았다

Error:Cause: failed to find target with hash string ‘Google Inc.:Google APIs:21’ in:

Windows7, Android Studio 2.2.3

Usually I can see that message when I don’t have Google Api 21 in my sdk directory, but if I open SDK Manager I have it like this:1484714680794

In this case, click Launch Standalone SDK Manager,

1484714915662

I found out that I had “broken” Google Api 21 in Extras group (I’m afraid I don’t have image). I deleted it and installed it in SDK Manager then I don’t see that error any more.

 

Error:Cause: failed to find target with hash string ‘Google Inc.:Google APIs:21’ in:

LSA vs Streamsupport

Java8 Lightweight-Stream-API Streamsupport
java.util.function com.annimon.stream.function java8.util.function
BiConsumer<T,U> BiConsumer BiConsumer
BiConsumers
BiFunction<T,U,R> BiFunction BiFunction
BiFunctions
BinaryOperator<T> BinaryOperator BinaryOperator
BinaryOperators
BiPredicate<T,U> BiPredicate
BiPredicates
BooleanSupplier BooleanSupplier
Consumer<T> Consumer Consumer
Consumers
DoubleBinaryOperator DoubleBinaryOperator DoubleBinaryOperator
DoubleConsumer DoubleConsumer DoubleConsumer
DoubleConsumers
DoubleFunction<R> DoubleFunction DoubleFunction
DoublePredicate DoublePredicate DoublePredicate
DoublePredicates
DoubleSupplier DoubleSupplier DoubleSupplier
DoubleToIntFunction DoubleToIntFunction DoubleToIntFunction
DoubleToLongFunction DoubleToLongFunction DoubleToLongFunction
DoubleUnaryOperator DoubleUnaryOperator DoubleUnaryOperator
DoubleUnaryOperators
Function<T,R> Function Function
Functions
FunctionalInterface
IntBinaryOperator IntBinaryOperator IntBinaryOperator
IntConsumer IntConsumer IntConsumer
IntConsumers
IntFunction<R> IntFunction IntFunction
IntPredicate IntPredicate IntPredicate
IntPredicates
IntSupplier IntSupplier IntSupplier
IntToDoubleFunction IntToDoubleFunction IntToDoubleFunction
IntToLongFunction IntToLongFunction IntToLongFunction
IntUnaryOperator IntUnaryOperator IntUnaryOperator
IntUnaryOperators
LongBinaryOperator LongBinaryOperator LongBinaryOperator
LongConsumer LongConsumer LongConsumer
LongConsumers
LongFunction<R> LongFunction LongFunction
LongPredicate LongPredicate LongPredicate
LongPredicates
LongSupplier LongSupplier LongSupplier
LongToDoubleFunction LongToDoubleFunction LongToDoubleFunction
LongToIntFunction LongToIntFunction LongToIntFunction
LongUnaryOperator LongUnaryOperator LongUnaryOperator
LongUnaryOperators
ObjDoubleConsumer<T> ObjDoubleConsumer ObjDoubleConsumer
ObjIntConsumer<T> ObjIntConsumer ObjIntConsumer
ObjLongConsumer<T> ObjLongConsumer ObjLongConsumer
Predicate<T> Predicate Predicate
Predicates
Supplier<T> Supplier Supplier
ThrowableConsumer
ThrowableFunction
ThrowablePredicate
ThrowableSupplier
ToDoubleBiFunction<T,U> ToDoubleBiFunction
ToDoubleFunction<T> ToDoubleFunction ToDoubleFunction
ToIntBiFunction<T,U> ToIntBiFunction
ToIntFunction<T> ToIntFunction ToIntFunction
ToLongBiFunction<T,U> ToLongBiFunction
ToLongFunction<T> ToLongFunction ToLongFunction
UnaryOperator<T> UnaryOperator UnaryOperator
UnaryOperators
java.util.Random
java8.lang.Doubles
java8.lang.FunctionalInterface
java8.lang.Integers
java8.lang.Iterables
java8.lang.Longs
java.util com.annimon.stream java8.util
ArrayDequeSpliterator
ArrayListSpliterator
ArrayPrefixHelpers
ArraysArrayListSpliterator
ArraysParallelSortHelpers
Comparators
COWArrayListSpliterator
COWArraySetSpliterator
DelegatingSpliterator
DoubleSummaryStatistics
DualPivotQuicksort
Objects Objects Objects.java
Observable
Optional<T> Optional Optional.java
OptionalDouble OptionalDouble OptionalDouble
OptionalInt OptionalInt OptionalInt
OptionalLong OptionalLong OptionalLong
PBQueueSpliterator
PQueueSpliterator
PrimitiveIterator
RASpliterator
Spliterator
Spliterators
SplittableRandom
StringJoiner
TimSort
UnsafeAccess
VectorSpliterator
java.util.concurrent   java8.util.concurrent
CompletionException CompletionException
ConcurrentMaps
CountedCompleter<T> CountedCompleter
ForkJoinPool ForkJoinPool
ForkJoinTask<V> ForkJoinTask
ForkJoinWorkerThread ForkJoinWorkerThread
Phaser Phaser
RecursiveAction RecursiveAction
RecursiveTask<V> RecursiveTask
ThreadLocalRandom ThreadLocalRandom
TLRandom
UnsafeAccess
java.util.stream com.annimon.stream java8.util.stream
BaseStream<T,S> BaseStream
Collector<T,A,R> Collector Collector
Collectors Collectors Collectors
Compat
DistinctOps
DoublePipeline
DoubleStream DoubleStream DoubleStream
DoubleStream.Builder
DoubleStreams
Exceptional
FindOps
ForEachOps
IntPipeline
IntStream IntStream IntStream
IntStream.Builder
IntStreams
IntPair
LazyIterator
LongPipeline
LongStream LongStream LongStream
LongStream.Builder LongStreams
LsaExtIterator
LsaIterator
MatchOps
Node
Nodes
PipelineHelper
PrimitiveExtIterator
PrimitiveIterator
RandomCompat
ReduceOps
ReferencePipeline
RefStreams
Sink
SinkConsumer
SinkDefaults
SliceOps
SortedOps
SpinedBuffer SpinedBuffer
Stream.Builder<T>
Stream<T> Stream Stream
StreamOpFlag
Streams
StreamShape
StreamSpliterators
StreamSupport StreamSupport
TerminalOp
TerminalOpDefaults
TerminalSink
WhileOps
LSA vs Streamsupport

RecyclerView and Scroll

리싸이클러뷰에 있는 여러가지 스크롤 기능을 정리해 보겠습니다. 레이아웃 매니져도 스크롤에 관여하는데 그 중에서도 LinearLayoutManager에 대해서만 정리합니다. 예제 코드는 https://github.com/BattleShipPark/example.rv_scroll에 있습니다

일단 스크롤 메소드를 표로 정리하고 각각에 대해 알아 봅니다. 각 메소드마다 API 문서의 설명을 붙여 놨습니다.

RecyclerView LinearLayoutManager
scrollBy() scrollHorizontallyBy()

scrollVerticallyBy()

scrollTo()
scrollToPosition() scrollToPosition()
scrollToPositionWithOffset()
smoothScrollBy()
smoothScrollToPosition() smoothScrollToPosition()

 

scrollBy()

public void scrollBy (int x, int y)
Move the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.

스크롤 콜백이 호출된다는 것과 실제로는 LayoutManager의 scrollHorizontallyBy()와 scrollVerticallyBy()를 호출해서 구현했다는 것이 특징입니다

scrollTo()

public void scrollTo(int x, int y)
View Set the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.

스크롤 콜백이 호출된다는 것이 특징입니다.

scrollToPosition()

public void scrollToPosition(int position)
Convenience method to scroll to a certain position. RecyclerView does not implement scrolling logic, rather forwards the call to RecyclerView.LayoutManager.scrollToPosition(int)

특정 위치로 이동할 수 있는데 실제 구현은 LayoutManager.scrollToPosition()으로 위임하고 있습니다. 아래는 LinearLayoutManager.scrollToPosition()입니다.

public void scrollToPosition(int position)
Scroll the RecyclerView to make the position visible.
RecyclerView will scroll the minimum amount that is necessary to make the target position visible. If you are looking for a similar behavior to android.widget.ListView.setSelection(int) or android.widget.ListView.setSelectionFromTop(int, int), use scrollToPositionWithOffset(int, int).
Note that scroll position change will not be reflected until the next layout call.

뭔가 설명이 긴데 예제를 보겠습니다. 예제앱에서 리스트의 처음 상태는 아래와 같습니다.

1

여기서 scrollToPosition() 버튼을 누르면 6번으로 이동하게 됩니다. 그런데 6번이 오른쪽으로 붙네요?

2

기대했던건 왼쪽에 붙는건데 다시 버튼을 눌러서 12번, 18번으로 이동해 보면 동일하게 오른쪽으로 붙습니다.

34

다시 한 번 버튼을 누르면 4번으로 이동하는데, 이번에는 왼쪽으로 붙네요?

5

아.. 문서에 있는 minimum amount, target position visible이 이런 뜻이군요. 해당 아이템이 화면에 보이도록 최소한만 움직이는 겁니다. 처음에는 6번이 화면 오른쪽에 숨어 있었기 때문에 최소한으로 움직이면 6번이 화면 오른쪽에 붙는거고, 12번, 18번도 마찬가지입니다. 그 다음 4번은 화면 왼쪽에 숨어 있기 때문에 화면 왼쪽에 붙는게 최소한으로 움직이는거죠.

기존의 ListView.setSelection()에서는 화면 왼쪽으로 붙여 줬는데, 그 기능은 scrollToPositionWithOffset()을 사용하라는군요. 확인해 볼까요?

scrollToPositionWithOffset()

public void scrollToPositionWithOffset(int position, int offset)
Scroll to the specified adapter position with the given offset from resolved layout start. Resolved layout start depends on getReverseLayout(), ViewCompat.getLayoutDirection(View) and getStackFromEnd().
For example, if layout is VERTICAL and getStackFromEnd() is true, calling scrollToPositionWithOffset(10, 20) will layout such that item[10]’s bottom is 20 pixels above the RecyclerView’s bottom.
Note that scroll position change will not be reflected until the next layout call.
If you are just trying to make a position visible, use scrollToPosition(int).

설명이 엄청 기네요.. 일단 scrollToPositionWithOffset() 버튼을 눌러 봅시다. offset은 20픽셀로 되어 있습니다.

11121314

scrollToPosition()과는 다른 결과입니다. 무조건 화면 왼쪽으로 붙고 offset만큼 떨어져 있네요. 이해하기에는 어렵지 않은 결과인데, 문서에 있는 getReverseLayout()과 getStackFromEnd()는 무슨 기능일까요? 아래에서 확인해 봅니다.

getReverseLayout()

reverseLayout() 버튼을 눌러서 새 액티비티를 띄우고 체크박스를 켜고 끄면서 실행해 봅시다.

reverseLayout()을 켜면 아래 그림처럼 0번 아이템이 화면 오른쪽과 아래에서부터 시작합니다. 그리고 scrollToPosition()과 scrollToPositionWithOffset()을 사용해 보면 방향만 바뀌고 앞에서 설명한 로직대로 동작하는 것을 알 수 있습니다.

21

22

getStackFromEnd()

이번에는 stackFromEnd 버튼을 눌러서 새 액티비티를 띄워봅시다. scrollToPositionWithOffset() 문서에 보면 getStackFromEnd()에 따라 결과가 달라진다는데, 체크박스를 켜고 확인해 볼까요?

31

결과가 달라지나요? 제가 볼때는 똑같은데… 문서로 봐서는 VERTICAL이고 getStackFromEnd()==true이면 아래쪽에 붙어야 하는데 차이가 없네요. 검색해 봐도 별 내용 없는데 ㅜㅜ

그 와중에 활용할 수 있는 예를 하나 찾았는데요, 아래 그림은 add() 버튼을 누를 때 아이템을 추가하도록한 예제입니다. 그런데 아이템이 추가되도 스크롤이 맨 끝으로 유지되고 있죠? 즉 채팅처럼 아이템을 끝에 추가할 때 스크롤을 맨 끝으로 유지할 수 있는 방법입니다. 직접 스크롤을 제어하지 않더라도, setStackFromEnd(true)이면 맨 끝의 아이템을 볼 수 있습니다. 다만 여기서도 Adapter.notifyDataSetChanged()를 호출하면 안 되고 RecyclerView.setAdapter()로 어댑터를 바꿔줘야 이런 효과가 가능합니다.

32

smoothScrollBy()

public void smoothScrollBy(int dx, int dy)
Animate a scroll by the given amount of pixels along either axis.

부드러운(smooth) 스크롤을 합니다. 아래 메소드를 더 자세히 살펴보겠습니다.

smoothScrollToPosition()

public void smoothScrollToPosition(int position)
Starts a smooth scroll to an adapter position.
To support smooth scrolling, you must override RecyclerView.LayoutManager.smoothScrollToPosition(RecyclerView, RecyclerView.State, int) and create a RecyclerView.SmoothScroller.
RecyclerView.LayoutManager is responsible for creating the actual scroll action. If you want to provide a custom smooth scroll logic, override RecyclerView.LayoutManager.smoothScrollToPosition(RecyclerView, RecyclerView.State, int) in your LayoutManager.

문서를 보면 LayoutManager.smoothScrollToPosition()을 오버라이드 하라고 하는데, 핵심은 SmoothScroller를 사용하는 것입니다. 실제 LayoutManager.smoothScrollToPosition()은 아래처럼 구현되어 있습니다

LinearSmoothScroller linearSmoothScroller =
        new LinearSmoothScroller(recyclerView.getContext()) {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                return LinearLayoutManager.this
                        .computeScrollVectorForPosition(targetPosition);
            }
        };
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);

여기서 LinearSmoothScroller는 SmoothScroller의 서브클래스입니다. 그럼 오버라이드하면 유용한 메소드 몇 개를 살펴보겠습니다. 예제에서는 아래와 같이 사용했습니다.

@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
    return layoutManager.computeScrollVectorForPosition(targetPosition);
}

@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
    return super.calculateSpeedPerPixel(displayMetrics) * 3;
}

@Override
public int calculateDxToMakeVisible(View view, int snapPreference) {
    return super.calculateDxToMakeVisible(view, SNAP_TO_START) + centerOffset;
}

public PointF computeScrollVectorForPosition(int targetPosition)

이건 추상 메소드라서 무조건 구현해야 합니다만, 위에서처럼 LayoutManager의 코드를 사용하면 됩니다. 현재 위치에 따라 어느 방향으로 스크롤할 것인가를 결정하는건데 특별히 커스텀할 상황이 생각나지는 않네요.

protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics)

스크롤 속도를 조절할 수 있습니다. 픽셀당 움직이는 속도(ms)이므로 클수록  천천히 이동합니다. 기본 구현 코드가 있으므로 그걸 적당히 키워서 사용하면 됩니다.

public int calculateDxToMakeVisible(View view, int snapPreference)

이름 그대로 아이템이 완전히 보이기 위해 필요한 이동량입니다. 이건 Dx니까 횡스크롤에서 필요할거고, 종스크롤에서는 Dy메소드를 사용하면 됩니다.

이 메소드를 굳이 소개한 이유는, 이 글의 맨 위에 표를 다시 보시면 현재 LinearLayoutManager에는 smoothScrollToPositionWithOffset()이 없는데, 이 메소드를 사용하면 필요한 기능을 제공할 수 있습니다. 위에 예제에서처럼 offset을 더해주면 됩니다.

그럼 아이템이 어디를 기준으로 하고 있는가도 중요한데요, SNAP_TO_START가 그 역할을 합니다. 가능한 값은 아래와 같습니다.

SNAP_TO_START: Align child view’s left or top with parent view’s left or top

SNAP_TO_END: Align child view’s right or bottom with parent view’s right or bottom

SNAP_TO_ANY: Decides if the child should be snapped from start or end, depending on where it currently is in relation to its parent.
For instance, if the view is virtually on the left of RecyclerView, using SNAP_TO_ANY is the same as using SNAP_TO_START

스크롤 타이밍

각 스크롤 메소드에 대해 알아 봤는데요, scrollToPosition()과 scrollToPositionWithOffset() 문서를 보면 “Note that scroll position change will not be reflected until the next layout call.” 이런 설명이 있습니다. 이건 어떤 영향을 줄까요?

예제에서 “scrollToPosition(), notifyDataSetChanged()” 버튼을 누르면, 1번 인덱스로 스크롤하고 1번 위치에 20이라는 아이템을 추가하도록 했습니다. 그럼 1번 아이템이 안 보이는 상태에서 버튼을 누르면 1번이 화면에 보여야 할 것 같은데, 아래처럼 20번이 화면에 보입니다.

41

LinearLayoutManager.scrollToPosition() 코드를 보면

mPendingScrollPosition = position;
mPendingScrollPositionOffset = INVALID_OFFSET;
if (mPendingSavedState != null) {
    mPendingSavedState.invalidateAnchor();
}
requestLayout();

바로 스크롤하는게 아니라 일단 멤버에 저장하고 넘어가는걸 알 수 있는데요, 즉 예제의 경우에서는 1번 인덱스로 스크롤하도록 요청했지만, 해당 인덱스에는 20이 들어간 후에 리스트를 그리면서 스크롤 처리를 하므로 20번 기준으로 스크롤이 되는 것입니다.

자주 일어나는 상황은 아닌데, 데이터 변경과 스크롤을 동시에 해야 하는 상황에서 마음대로 스크롤이 안 되니 당황스러웠습니다 ^^;

긴 글 읽어 주셔서 감사합니다.

RecyclerView and Scroll

BATTLESHIP DEVDIARY VOL.5

RecyclerView에서 특정 아이템을 화면 가운데로 스크롤 (scroll to center with specified item in RecyclerView)

횡스크롤에서 갯수가 동적인 여러개의 그룹을 표현하는 상황이였습니다. 즉

I1 I2 I3 … 에서 I1을 즐겨 찾기를 하면

F1 Div I1 I2 I3 … 가 됩니다. 여기서 Div는 구분선인데, 아이템과 너비가 다릅니다. 게다가 특정 조건이 되면 프로모션을 위해 맨 앞에 아이템이 추가됩니다.

P1 Div I1 I2 D I3 I4 Div I5 I6 …

이 때 I5를 탭하면 I5가 화면 가운데로 스크롤하고 싶었습니다.

LayoutManager.scrollToPosition()은 아이템이 화면에 딱 걸리는 경우에 사용할 수 있으므로 지금은 적절하지 않습니다. scrollToPositionWithOffset()을 사용하면 된다고 하는건 금방 찾아냈는데… 이제 offset을 어떻게 계산한다?

화면 왼쪽이 기준이니까 선택한 아이템이 가운데에 왔을 때  화면 왼쪽에 걸리는 아이템을 찾아야 하는군. 그리고 선택한 아이템이 가운데로 오도록 offset을 계산하면 되는데.. 아이템마다 너비가 다르니 화면 왼쪽에 걸리는 아이템을 찾아 가기도 쉽지 않고, 게다가 상황마다 아이템 갯수도 다르니 고려해야 할게 점점 많아진다…. 이게 이렇게 복잡하게 할 필요가 있나 하는 생각에 포기하려는 순간….!

복잡하게 계산할 것 없이 그냥 선택한 아이템 자체만 고려해서 계산하면 되는거네요… 아래처럼 한 줄로.. 하아…

LayoutManager.scrollToPositionWithOffset(position, (screenWidth + itemWidth) / 2);

안드스튜디오 디자이너에서 뷰 배경에 그라데이션 넣기 (Gradation in view background in Android Studio Designer)

리소스에 <shape><gradient>를 넣으면 그라데이션 효과를 쉽게 넣을 수 있습니다. 디자인 가이드에 맞게 angle을 어떻게 넣어야 하는지 궁금해서 이러저리 값을 바꾸면서 안드로이드 스튜디오 디자이너에서 확인해 보는데, 제대로 적용이 안 되는겁니다.. 그래서 startColor와 endColor에 알파를 잘못 넣었나 싶어서 바꿔보고, centerX, centerY를 넣어야 하는건지 싶어서 바꿔보고, 한참 해봐도 뭐가 제대로 된 설정인지 알 수가 없었습니다.

그러다 혹시나 해서 빌드를 해서 단말에서 실행해 보니 색깔이 잘 나옵니다… 안드로이드 스튜디오의 디자이너 오류인가 보네요.. 슬프다..

BATTLESHIP DEVDIARY VOL.5