2015년 7월 13일 월요일

안드로이드 Custom Component 만들기

안드로이드 개발을 하다보면 Custom Component 혹은 Custom View를 만들일이 간혹 발생합니다. 가능하면 기본 컴포넌트를 사용하는게 좋지만 불가피할 경우에는 만들어야 합니다. 처음에 커스텀뷰를 만들때 Android 공식홈페이지의 글을 읽었지만 이론적으로만 습득하고 실제로는 어떻게 구현해야하는지 몰라 막막했었는데 이 글을 통해 처음 안드로이드 커스텀뷰를 만들어보시는 분들에게 도움이 되었으면 합니다.

꼭 Android 공식홈페이지의 글을 읽어보시기 바랍니다. onDraw, onMeasure등 커스텀 뷰를 만들기 위한 필수적인 지식들이 있습니다.


그러면 Android Library GearSlider를 만드는 과정을 통해 커스텀 컴포넌트를 요구사항 분석부터 구현까지 보여드리도록 하겠습니다.

"인스타그램 ADJUST 화면아래 조정하는거 이쁘던데, 그거 적용해 보는게 어떨까?" 라는 막연한 문장에서 부터 시작되었습니다. 


요구사항분석

저는 작업을 시작하기 전에 노트를 활용합니다. 어떤 클래스가 필요하고 어떤 뷰들의 조합으로 구성이 될 수 있을까?
gearslider를 만들때 제일 처음에 그려본것


단계 1: 컴포넌트를 한, 두 문장으로 정의하기
"손가락을 좌우로 움직여서 막대기 하나마다 value를 1씩 증가시키는 슬라이더" 라고 정리를 해보았습니다.

단계 2: 컴포넌트를 만들기 위한 준비물 리스트 만들기
  1. 터치이벤트를 처리할 View 1
  2. 자처럼 생긴 뒤에서 움직여줄 View 2

처음에는 이렇게 View 2개로 작업하기로 결정했습니다.

단계 3: 컴포넌트의 이동을 상상

"View 1에 scroll event를 주어서 View 2의 x값을 조정하는 방식으로 구현하자"

저는 보통 (1)컴포넌트가 어떠한 형태로 배치가 될지 생각을 해보고 (2)사용자의 입력값에 따라 뷰가 어떻게 이동할지 고민합니다. 작업의 우선순위는 View 1을 만들고 View 2를 생성해야 겠다고 생각했습니다. View 1의 터치이벤트가 가장 먼저 보장되어야 View 2를 움직이는게 의미가 있기 때문입니다.

단계 4: 필요한 기능을 정의 및 구현방법 생각

어떠한 뷰가 필요할지 생각한 후 어떠한 형태로 움직일지 정해졌다면 마지막으로 기능적인 부분을 생각합니다. 

"View 1의 가운데 위치( view1_x+(view1_width/2) )와 View 2의 view2_x 값에 따라 값을 전달하도록 하자."


프로젝트 생성 및 구현

연습용 프로젝트를 하나 생성합니다.

View 1 구현

FrameLayout 을 활용하는 것이 좋다고 판단되어 FrameLayout 을 상속받았습니다.
 public class View1 extends FrameLayout{  
   public View1(Context context, AttributeSet attrs) {  
     super(context, attrs);  
   }  
 }  

GestureDetector.SimpleOnGestureListener를 활용해 scroll 이벤트를 처리하도록 합니다.
 GestureDetector.SimpleOnGestureListener  

setClickable(true)를 통해 클릭이벤트를 처리할 수 있도록 해줍니다. 그리고 onScroll의 e1, e2, distanceX 를 활용해 사용자가 오른쪽, 왼쪽 으로 손가락을 이동하는 모션을 처리하도록 합니다.

 public class View1 extends FrameLayout{  
   private GestureDetectorCompat mDetector;  
   public View1(Context context, AttributeSet attrs) {  
     super(context, attrs);  
     setClickable(true);  
     mDetector = new GestureDetectorCompat(getContext(), new View1GestureListener());  
   }  
   @Override  
   public boolean onTouchEvent(MotionEvent event) {  
     return mDetector.onTouchEvent(event) || super.onTouchEvent(event);  
   }  
   class View1GestureListener extends GestureDetector.SimpleOnGestureListener {  
     @Override  
     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,  
                 float distanceY) {  
       if (distanceX < 0) {  
         Log.i("TAG", "Left to Right");  
       } else{  
         Log.i("TAG", "Right to Left");  
       }  
       return true;  
     }  
   }  
 }  


자신의 layout 파일에 아래와 같이 컴포넌트를 추가해서 터치이벤트가 잘 먹히는지 확인해봅니다.
   <개인의.패키지명.View1  
     android:background="@android:color/black"  
     android:layout_width="match_parent"  
     android:layout_height="70dp" />  

스크롤 이벤트 탐지가 가능해 졌다면 "손가락을 좌우로 움직여서 막대기 하나마다 value를 1씩 증가시키는 슬라이더"에서 손가락을 좌우로 움직여서라는 작업은 만족하게 되었습니다.

View 2 구현

View 2의 경우는 onDraw()와 onMeasure()를 오버라이드 하여 적절하게 생성해 줍니다. 막대기의 개수는 20개 막대기의 간격은 100으로 지정하였습니다.

 public class View2 extends View {  
   public static int mNumberOfBar = 20;  
   public static int mIntervalOfBar = 100;  
   private float width, height;  
   private Paint paint = new Paint();  
   public View2(Context context) {  
     super(context);  
   }  
   @Override  
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
     width = mIntervalOfBar * mNumberOfBar;  
     height = MeasureSpec.getSize(heightMeasureSpec);  
     setMeasuredDimension((int) width, (int) height);  
   }  
   @Override  
   public void onDraw(Canvas canvas) {  
     paint.setStyle(Paint.Style.STROKE);  
     paint.setStrokeWidth(3);  
     paint.setAntiAlias(false);  
     paint.setColor(Color.BLACK);  
     for (int i = 0; i <= mNumberOfBar; ++i) {  
       float x = i * mIntervalOfBar;  
       canvas.drawLine(x, 0, x, height, paint);  
     }  
   }  
 }  

View 2를 만들었다면 View 1이 View 2를 갖도록 작업을 해야합니다.

View 1에 View 2를 addView()하기

View 1이 xml파일로부터 inflate가 마무리 되는 시점에 View2를 추가해주도록 합니다. onFinishInflate()는 inflate가 마무리 되는 시점에 실행됩니다.
 private View2 mView2;  
 @Override  
 protected void onFinishInflate() {  
   super.onFinishInflate();  
   mView2= new View2(getContext());  
   addView(mView2);  
 }  


View 1의 배경색을 투명한색으로 변경한 후에 실행을 해봅니다.
   <org.nhnnext.josunghwan.sampleproject.View1  
     android:background="@android:color/transparent"  
     android:layout_width="match_parent"  
     android:layout_height="70dp" />  


위와 같이 View 1의 터치이벤트는 입력을 받을 수 있고 View 1의 아래에 View 2가 생성된 모습을 확인하실 수 있습니다.

하지만 현재는 View 1의 터치이벤트와 View 2의 움직임에는 아무런 관련이 없습니다. 이제 View 1의 onScroll()에 코드를 작성하여 View 2의 위치를 변경할 수 있도록 수정하겠습니다.

View 1의 터치이벤트와 View 2의 위치이동을 연결하자


View 1의 onScroll() 코드 부분을 수정하였습니다. (1)View 1의 가운데(view1_width/2)보다 View 2의 x값 - distanceX가 클 경우에는 Too Low를 출력하고 이동하지 않습니다. (2)View 1의 가운데(view1_width/2-1)보다 View 2의 x값 + View 2 - distance의 가로 길이가 작을 경우에는 Too High를 출력하고 이동하지 않습니다. (3) 위의 경우가 아닐때에는 이동된 거리(distanceX)만큼 View 2의 위치를 옮겨주도록 합니다.

 @Override  
     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,  
                 float distanceY) {  
       if (getWidth() / 2 < (mView2.getX() - distanceX)) {  
         Log.i("TAG", "Too Low");  
       } else if ((mView2.getX() + mView2.getWidth()) - distanceX < (getWidth() / 2) - 1) {  
         Log.i("TAG", "Too High");  
       } else {  
         mView2.setX(mView2.getX() - distanceX);  
       }  
       return true;  
     }  

이제 손가락을 움직일때마다 View 2가 이동되는 모습을 확인하실 수 있습니다.

사용자의 입력에 따라 View가 움직이는 것은 구현이 되었지만, 우리의 어플리케이션은 보여주는 것만으로는 만족할 수 없습니다. View가 갖고있는 값이 무엇인지 알려주어야 합니다.

View 가 갖고있는 값을 갱신하자

int mCurrentValue 를 선언하여 변수값을 저장할 수 있도록합니다. onScroll()의 로직은 원하시는 대로 작성하시면 됩니다. 아래의 코드는 막대기를 넘어갔을때 "Tick"을 출력하게 했고, 가까운 막대기의 값으로 mCurrentValue를 갱신시키는 코드입니다.

 private int mCurrentValue;  
 class View1GestureListener extends GestureDetector.SimpleOnGestureListener {  
   float rulerPosition;  
   @Override  
   public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,  
               float distanceY) {  
     if (getWidth() / 2 < (mView2.getX() - distanceX)) {  
       Log.i("TAG", "Too Low");  
     } else if ((mView2.getX() + mView2.getWidth()) - distanceX < (getWidth() / 2) - 1) {  
       Log.i("TAG", "Too High");  
     } else {  
       int previousValue = (int) (rulerPosition / mView2.mIntervalOfBar);  
       rulerPosition = (mView2.getX() * -1) + (getWidth() / 2);  
       if (previousValue != (int) (rulerPosition / mView2.mIntervalOfBar))  
         Log.i("TAG", "Tick!!");  
       mCurrentValue = Math.round(View2.mNumberOfBar * (rulerPosition) / mView2.getWidth());  
       mView2.setX(mView2.getX() - distanceX);  
     }  
     return true;  
   }  
 }  
 public int getValue(){  
   return mCurrentValue;  
 }  

이렇게 되면 코드는 어느정도 마무리 된것 같습니다. 하지만, 스크롤될때 값이 변경되는것을 다른 컴포넌트에게 알려줄 필요가 있으므로 리스너를 구현하도록 하겠습니다.

리스너를 구현하자

View1에 리스너 인터페이스를 작성합니다. 그리고 리스너를 셋팅할 수 있는 변수와 메서드를 추가합니다.
 private OnValueChangeListener mListener;  
 public interface OnValueChangeListener {  
   void onValueChange(int value);  
 }  
 public void setChangeValueListener(OnValueChangeListener listener) {  
   mListener = listener;  
 }  

그리고 onScroll()에 값이 변경되었을때 리스너의 onValueChange(value)를 실행할 수 있도록 합니다. (코드상 볼드 처리한 곳)

 @Override  
 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,  
             float distanceY) {  
   if (getWidth() / 2 < (mRulerView.getX() - distanceX)) {  
     Log.i(DEBUG_TAG, "Too Low Value");  
   } else if ((mRulerView.getX() + mRulerView.getWidth()) - distanceX < (getWidth() / 2) - DPSIZE) {  
     Log.i(DEBUG_TAG, "Too High Value");  
   } else {  
     int previousValue = (int) (rulerPosition / mIntervalOfBar);  
     rulerPosition = (mRulerView.getX() * -1) + (getWidth() / 2);  
     if (previousValue != (int) (rulerPosition / mIntervalOfBar)) playTickSound();  
     mCurrentValue = Math.round(mNumberOfBar * (rulerPosition) / mRulerView.getWidth());  
     mRulerView.setX(mRulerView.getX() - distanceX);  
     if (mListener != null) {                                 
       mListener.onValueChange(mCurrentValue);                
     }  
   }  
   return true;  
 }  

이제 Custom Component 은 다 만들어 졌습니다! 메인액티비티에 TextView를 추가하고 View1에게 리스너를 등록하여 TextView가 변경되도록 작업 해보겠습니다.

마무리. 예제 만들어보기

activity_main.xml

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"  
   android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"  
   android:paddingRight="@dimen/activity_horizontal_margin"  
   android:paddingTop="@dimen/activity_vertical_margin"  
   android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">  
   <org.nhnnext.josunghwan.sampleproject.View1  
     android:id="@+id/custom_component"  
     android:background="@android:color/transparent"  
     android:layout_width="match_parent"  
     android:layout_height="70dp" />  
   <TextView  
     android:id="@+id/tv1"  
     android:layout_below="@id/custom_component"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content" />  
 </RelativeLayout>  

MainActivity.java

 public class MainActivity extends AppCompatActivity {  
   @Override  
   protected void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.activity_main);  
     final TextView tv = (TextView) findViewById(R.id.tv1);  
     View1 v1 = (View1) findViewById(R.id.custom_component);  
     v1.setChangeValueListener(new View1.OnValueChangeListener() {  
       @Override  
       public void onValueChange(int value) {  
         tv.setText(String.format("Value : %d", value));  
       }  
     });  
   }  
 }  

값을 7로

값을 20로


이렇게 우리는 요구사항부터 분석하며 Custom Component 를 직접 구현해 보았습니다. 현재 위의 코드 외에도 더 많은 요구사항들이 추가 될 수 있습니다. 막대기를 넘어갈 때 사운드를 출력해라, 가속도에 따라 막대가 움직이는 것을 조정해라, 막대기 근처에 가면 자동으로 막대기로 이동하도록 하여라, 막대기의 길이를 정할 수 있게 해줘라, 애니메이션을 추가해라, xml에서 속성값을 지정할 수 있게 해라 등등 다양한 요구사항이 있고 실제 그것을 만족하는 컴포넌트가 있다고 하더라도 처음에는 가장 핵심적인 기능부터 구현해 나가면 쉽다. 라는것을 전달해 드리고 싶었습니다.  View 1의 경우 GearSlider의 GearSlider.java이고 View 2의 경우 RulerView.java 입니다.  원래는 가운데에 선을 만들고 좌우로 점점 투명해지는 효과를 주는 CenterBar도 같이 작성하려했는데 핵심기능은 아닌거 같아 추가하지 않았습니다.

*본문에 있는 View1, View2, tv, v1등 클래스명과 변수명은 개인적으로는 좋아하진 않지만 괜히 복잡한 이름 때문에 본문을 읽는데 방해가 될까봐 단순하게 작성하였습니다.

GearSlider 코드를 보시면서 새로운 기능을 추가해보시는 것도 도움이 꽤 될 것입니다. 안드로이드 커스텀 컴포넌트를 처음 만들어 "답답했을때 이러한 자료가 있으면 좋겠다"라고 생각했는데 생각했던 것만큼 잘 전달될지는 모르겠습니다. 

2015년 7월 8일 수요일

jCenter에 안드로이드 라이브러리 배포하기

Android Library GearSlider를 만든 후 jcenter에 배포한 경험을 공유하기 위해 글을 작성합니다. 구글에 검색하여 나온 블로그 글을 참조했는데 중간에 막히는 부분이 있어서 다른 배포도구를 활용했습니다.

이 문서의 최종 목표는

1:  dependencies {  
2:    compile '나만의.안드로이드.라이브러리'  
3:  }  

위와 같이 build.gradle 파일에 작성함으로써 자신의 라이브러리를 다른 사람들과 쉽게 공유할 수 있게 만드는 것에 있습니다. 이 글을 읽기 전에 Android 공식 홈페이지의 글을 먼저 읽어 보시길 바랍니다.

단 한줄만으로 라이브러리를 가져올수 있다는 것은 굉장히 유용합니다. 그렇다면 우리는 어디에 라이브러리를 올려두어야 다른 안드로이드 개발자들이 쉽게 가져올 수 있을까요.

Android Studio는 어디에서 라이브러리를 가져오는 걸까?


Android Studio는 build.gradle 에 작성해둔 Maven Repository Server에서 라이브러리를 가져옵니다. 일반적으로 안드로이드는 jcenterMaven Central에서 라이브러리를 가져옵니다.

jcenter
jcenter는 bintray.com이 운영하는 Maven Repository입니다. jcenter의 라이브러리를 사용하기 위해서는 build.gradle 파일에 아래와 같이 정의해야 합니다.

1:  allprojects {  
2:    repositories {  
3:      jcenter()  
4:    }  
5:  }  


Maven Central
Maven Central은 sonatype.org이 운영하는 Maven Repository입니다.  Maven Central의 라이브러리를 사용하기 위해서는 build.gradle 파일에 아래와 같이 정의해야 합니다.

1:  allprojects {  
2:    repositories {  
3:      mavenCentral()  
4:    }  
5:  }  

jcenter와 Maven Central은 둘다 표준 안드로이드 라이브러리 저장소들이지만 완전히 다른 장소에서 운영됩니다. 서로 아무런 연관이 없습니다. (jcenter는 Maven Central 저장소에 배포하는 기능을 제공하긴합니다.)

그 외의 곳에서는?
그 외의 다른 저장소로부터 라이브러리를 가져오기 위해선 아래와 같이 저장소의 위치를 명시해주어야 합니다.

1:  repositories {  
2:    maven { url '다른저장소의주소' }  
3:  }  

그렇게 하면 그 저장소에 배포되어있는 다른 라이브러리를 가져올수 있습니다.
1:  dependencies {  
2:    compile '다른저장소에있는.라이브러리'  
3:  }  


그렇다면, 표준서버와 개인서버에 배포하는것 중 무엇이 좋을까요? 개인적으로 사용해야하는 라이브러리는 개인서버에 배포하는것이 낫고, 다른 개발자들과 함께 나누고 싶다면 표준서버에 배포하는것이 좋을겁니다. 그냥 개인의 판단에 따라 마음에 드는곳에 하시면됩니다. 이 글에서는 jCenter에 배포하는법에 대해서 다룹니다.

jcenter와 Maven Central에 대한 이해


jcenter와 Maven Central은 Java/Android 라이브러리들을 배포한다는 공통점을 가지고 있습니다.

초반에 Android Studio에서는 Maven Central을 기본 저장소로 선택했습니다. 과거버전 Android Studio 프로젝트의 build.gradle에서는 mavenCentral()이 정의 되어있을 것입니다.

하지만 Maven Central은 개발자 친화적이지 않았습니다. 라이브러리를 업로드하는것이 정말정말정말로 어렵고 geeky의 레벨에 도달한 개발자들이나 가능했습니다. 그리고 기타등등 문제들 때문에 Android Studio 팀은 jcenter를 기본 저장소로 변경하기로 결정했습니다.

Maven Central에서 jcenter로 변경한것에는 다양한 이유가 있습니다

  1. jcenter는 CDN을 통해 라이브러리를 배포합니다. 따라서 개발자들은 빠른 로딩속도를 즐길수 있습니다.
  2. jcenter는 지구상에서 가장큰 Java Repository입니다. 
  3. 저장소에 라이브러리를 업로드하는것이 정말정말정말 쉽습니다.
  4. 친근한 UI !
  5. 만약 Maven Central에 라이브러리를 배포하고싶다면 bintray site에서 한번의 클릭을 통해 배포할 수 있습니다. ( 한번 설정을 해주어야하긴 합니다)
위와 같은 이유 때문에 jcenter를 기본저장소로 지정한것이 굉장히 좋은판단이였다고 생각됩니다. 라고 하네요. (왜 처음에는 Maven Central를 지정했는지 의문입니다) 


gradle은 어떻게 저장소로부터 라이브러리를 가져올까?


어떻게 jcenter로 라이브러리를 업로드하는지 말하기전에. gradle이 어떻게 저장소로부터 라이브러리를 가지고 오는지 알아봅시다. 예를들어 build.gradle에 아래와 같이 적혀있다면, 그 프로젝트에 해당 라이브러리 파일이 다운로드 됩니다.

1:  compile 'org.nhnnext.sunghwanjo:gearslider:0.1.1'  

기본적으로 라이브러리를 가져오기 위한 문자열은 3개의 부분으로 구성되어 있습니다.

1:  GROUP_ID:ARTIFACT_ID:VERSION  

위에서 GROUP_ID는 org.nhnnext.sunghwanjo 입니다. ARTIFACT_ID는 gearslider이고 VERSION은 0.1.1이죠.

GROUP_ID는 라이브러리의 그룹이름을 의미합니다. 같은 문맥상 여러가지일을 하는 하나이상의 라이브러리가 있을 가능성이 있습니다. 이럴때 GROUP_ID를 공유하면 됩니다. 보통 개발자의 패키지명으로 합니다.
ARTIFACT_ID는 라이브러리의 이름입니다. VERSION은 버전정보인데 어떤 텍스트가 들어가도 상관없습니다. 다만, x.y.z의 형태로 작성하는 것을 추천합니다. 만약 본인이 원한다면 뒤에 -beta를 붙여도 괜찮습니다.

1:  dependencies {  
2:    compile 'com.squareup:otto:1.3.7'  
3:    compile 'com.squareup.picasso:picasso:2.5.2'  
4:    compile 'com.squareup.okhttp:okhttp:2.4.0'  
5:    compile 'com.squareup.retrofit:retrofit:1.9.0'  
6:  }  

위와 같이 의존성을 추가한다면 무슨일이 벌어질까요? Gradle은 Maven Repository Server에 원하는 라이브러리가 존재하는지 물어볼 것입니다. 존재한다면 주로 GROUP_ID:ARTIFACT_ID:VERSION_ID의 형태로 요청된 라이브러리의 경로를 얻을 것입니다.

그리고 Android Studio는 우리의 장비에 파일들을 다운로드하고 컴파일합니다. 그렇다면 라이브러리를 가져오는것은 끝!

이제 저장소로부터 library를 가져오는 방법에 대해서는 알지만 jar 와 aar 파일이 저장소 서버에 올려졌다는 것은 잘 모를것입니다. 또는 aar에 대해서 잘 모른다거나.

aar 파일 포맷을 알아보자


jar파일이 그냥 커피라면 aar 파일이 top 입니다.
aar 파일은 Android 라이브러리 프로젝트의 컴파일된 묶음파일입니다. 압축파일? Android 라이브러리는 AndroidManifest.xml, Resources, Assets, JNI처럼 jar 파일의 표준에는 벗어나는 안드로이드 종속적인 파일들을 포함할 필요가 있기 때문에 생겼습니다. 그래서 aar은 앞서 말한것들을 모두 포함합니다. jar 파일은 aar 파일안에 classes.jar 라는 이름으로 포함되어 있습니다. 아래는 파일 리스트 입니다.
  • /AndroidManifest.xml (mandatory)
  • /classes.jar (mandatory)
  • /res/ (mandatory)
  • /R.txt (mandatory)
  • /assets/ (optional)
  • /libs/*.jar (optional)
  • /jni/<abi>/*.so (optional)
  • /proguard.txt (optional)
  • /lint.jar (optional)
보시다시피 aar 파일은 안드로이드를 위해 특별히 설계되었습니다. 이 문서에서도 aar 형태의 라이브러리를 어떻게 만들고 배포하는지 작성할 것입니다.

jcenter에 나만의 라이브러리를 업로드하는 방법


이제 저장소에 라이브러리를 올리기 위한 기본적인 지식들은 갖고 있으니, 정말로 배포를 시작해봅시다. 우리의 목표는 간단합니다. "http://jcenter.bintray.com"에 우리의 라이브러리를 업로드 하는 방법. 입니다. 

해야할 단계가 좀 있긴한데 전체적으로 그렇게 어렵지는 않습니다. bintray가 정말 잘 만들어 놨거든요. 전체적인 시스템은 이렇습니다.

이미지출처 : http://inthecheesefactory.com/blog/how-to-upload-library-to-jcenter-maven-central-as-dependency/en

Android Studio에서 arr, pom 파일들을 빌드한후 Bintray로 업로드 합니다. 그리고 jcenter와 동기화. 그러면 끝입니다. Maven Central과 연동할 수도 있는데 그건 다루지 않습니다. 자료가 많고 되게 쉽거든요.

앞으로 할 작업들을 단계별로 나누어서 설명하도록 하겠습니다.

Part 1: Bintray에 패키지 만들기


첫째로. bintray에 패키지를 만들 필요가 있습니다. bintray 계정이 필요하고 웹사이트에 패키지를 만들겁니다.

단계 1: bintray.com 에 계정을 등록합니다. 

단계 2: 등록이 끝났으면, maven을 클릭합니다. (현재 저는 1개의 패키지가 등록되어있어서 (1)이 있습니다. )


단계 3: 우리의 라이브러리를 위한 패키지를 새롭게 생성합니다. Add New Package 클릭.

단계 4: 요구하는 정보를 모두 입력합니다.


패키지 이름을 정하는데 규칙은 없지만 일반적인 convention이 존재합니다. 모든 문자는 소문자여야 하고 단어 사이에는 하이픈(-)을 붙이도록 합니다. gear-slider 이렇게요.

필요한 정보를 모두 입력한후에 Create Package 를 누르면 됩니다.

단계 5: 정상적으로 완료되었다면 Edit Package로 이동될 것입니다. Edit Package 글자 아래 패키지명을 클릭하여 상세페이지로 이동합니다.


끝났습니다! 이제 우리는 라이브러리를 위한 Bintray Maven Repository를 가진것입니다. 이 공간에 업로드 되었다면 jcenter와 Maven Central에 업로드 하지 않아도 배포를 할 수 있긴합니다. (위에서 언급한 그 외의 장소에서 배포하는 경우를 활용하시면 됩니다.)


짜잔~. 하지만 현재 Versions 탭에는 아무런 정보가 없을것입니다. 위의 스크린샷의 경우 이미 배포를 한 상황이기 때문에 Versions탭에 정보가 있는것입니다.

Part 2: Android Studio Project 준비하기


이 문서를 읽는 분들은 라이브러리 프로젝트를 만드는 방법은 알것이라고 가정합니다. 하지만 GearSlider 프로젝트의 구조를 말씀드리자면 


GearSliderDemo라는 Project가 있습니다. 그 안에 Application Module인 app과 Library Module인 gearslider가 있습니다.

Application Module은 build.gradle에 아래와 같이 명시되어있고

1:  apply plugin: 'com.android.application'  

Library Module은 build.gradle에 아래와 같이 명시되어있습니다.

1:  apply plugin: 'com.android.library'  

Library Module은 보통 File-New-New Module을 통해 생성합니다. 



Application Module은 저장소에 배포하지 않을것이고 Library Module을 저장소에 업로드 할 것입니다. 즉, Library Module에 Gradle Task를 추가할 것입니다. 우리는 novoda:bintray-release 를 활용해서 좀 더 쉽게 배포할겁니다.

단계 1: 프로젝트의 build.gradle에 아래와 같은 문장을 추가합니다.

1:  dependencies {  
2:    ....  
3:    classpath 'com.novoda:bintray-release:0.3.0'     
4:  }  

단계 2: 라이브러리 모듈의 build.gradle에 아래와 같은 문장을 추가합니다. 
1:  apply plugin: 'com.android.library'  
2:  apply plugin: 'com.novoda.bintray-release'  
3:  publish {  
4:    userOrg = 'sunghwanjo'  
5:    groupId = 'org.nhnnext.sunghwanjo'  
6:    artifactId = 'gearslider'  
7:    publishVersion = '0.1.1'  
8:    desc = 'GearSlider UI for Android - It was inspired by the Adjust UI of Instagram.'  
9:    website = 'https://github.com/sunghwanJo/GearSlider'  
10:    issueTracker = "${website}/issues"  
11:    repository = "${website}.git"  
12:  }  

1번줄은 기본적으로 작성이 되어있을 것입니다. 2번줄은 bintray-release를 사용하기 위함이니 작성해줍니다. 4~11번 줄은 자신의 설정에 맞추어서 작성합니다.

설정이 완료되면 bintray.com으로 배포할 준비는 완료되었습니다. 이제 남은 작업은 2가지밖에 없습니다. bintray.com으로 배포하는 것과 jcenter와 연결하는 것.

Part 3: bintray에 라이브러리를 배포하자


단계 1: bintray.com에 배포하자.
novoda:bintray-release에서는 bintrayUpload라는 태스크를 실행시킴으로써 배포를 할 수 있게 도와줍니다. 

 $ ./gradlew clean build bintrayUpload -PbintrayUser=BINTRAY_USERNAME -PbintrayKey=BINTRAY_KEY -PdryRun=false  

위와같이 터미널에서 실행을 시키시면 bintray에 배포하는것은 완료된 것입니다.

BINTRAY_KEY는 bintray Profile Edit 페이지에 API KEY 메뉴를 클릭해서 확인하실 수 있습니다.

 이제 Bintray로 돌아가보면 Versions 영역에 새로운 버전이 추가되었을 것입니다. 



그리고 Files 탭에 들어가보면

라이브러리 파일이 성공적으로 들어가있는것을 확인하실 수 있습니다. (짝짝짝)

하지만, 아직 남은것이 있습니다. bintray에 배포되었다는 의미는 누군가 당신이 올린 라이브러리에 접근하기 위해선 build.gradle파일에 아래와 같이 저장소의 url을 지정해 주어야만합니다.
(저장소의 url은 위의 이미지를 보시면 How it works 아래에 있는 주소입니다. ex: https://dl.bintray.com/sunghwnjo/maven/)

 repositories {  
   maven {  
     url 'https://dl.bintray.com/sunghwanjo/maven/'  
   }  
 }  

https://dl.bintray.com/당신의이름/maven/ 이 bintray에서 당신의 라이브러리를 저장하는 장소입니다.

우리가 jcenter 에 라이브러리를 배포하게 된다면 굳이 위와같은 문장없이도 라이브러리를 가져올 수 있습니다. jcenter 는 안드로이드의 기본 라이브러리 저장소 이니깐요.

단계 2: jcenter와 연동하자(혹은 배포하자).

이미지를 따로 저장해 두지않아 위에서 언급한 블로그에서 이미지를 사용하도록하겠습니다.

아래와 같이 Add to JCenter를 클릭하게 되면 jcenter에 연결하는 팝업이 생성됩니다.





그리고 그냥 아무것도 안적어도 되고 적어도 되고 Send 버튼을 눌러줍니다.

이제 아침, 점심, 저녁 혹은 야식을 먹고 오시면 bintray 팀의 결과를 알 수 있습니다. 만약 연결이 되었다면
아래와 같은 jecenter 뱃지가 생길것입니다.


이제 jcenter에 업로드 되었기 떄문에 jcenter()가 명시되어있는 프로젝트에선 어떤 개발자든 아래의 한문장으로 라이브러리를 사용할 수 있습니다.

 compile 'org.nhnnext.sunghwanjo:gearslider:0.1.1'  

만약 jcenter안의 라이브러리 파일들을 확인하고 싶다면? http://jcenter.bintray.com 에서 확인해 볼 수 있습니다. 예를들어 org -> nhnnext -> sunghwanjo -> gearslider 이런식으로 말이죠.

jcenter와 연동했다면 패키지를 수정했을 경우에 자동으로 연동이 되어집니다. 한번만 연동하면 된다는 것이죠. 하지만, bintray 웹인터페이스에서 패키지 전체를 삭제하는 경우에는 jcenter의 library 파일들이 삭제되지 않습니다. 그러면 좀비파일이 되어 jcenter 서버에 둥둥 떠다니는 것이지요. 그러니 꼭 각 버전파일들을 전부 삭제한 후에 패키지를 삭제하는것을 추천드립니다.


이로써 jCenter에 안드로이드 라이브러리 배포하기 는 끝입니다. 자신만의 라이브러리를 만들어서 배포하고 싶은 개발자분들에게 도움이 되었으면 좋겠습니다. :D