Github : https://github.com/junghun9102/AndroidTemplate

Branch : library/retrofit

 

junghun9102/AndroidTemplate

Contribute to junghun9102/AndroidTemplate development by creating an account on GitHub.

github.com

 

 


Commit ea187676

 

기본 사용법

1. Service

2. OkHttpClient

3. Retrofit을 이용한 Service 구현체

 

1번과 3번만 구현하면 API통신이 가능하다.

2번은 선택적이지만 보통 구현한다.

 

GoogleNews RSS를 사용해 Retrofit 기본 사용법을 정리하려고 한다.

https://news.google.com/rss?hl=ko&gl=KR&ceid=KR:ko 

 

 

 

 

 


https://square.github.io/retrofit/

 

Retrofit

A type-safe HTTP client for Android and Java

square.github.io

dependencies

    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
    implementation 'com.squareup.okhttp3:okhttp:3.12.0'
    implementation 'com.squareup.retrofit2:converter-simplexml:2.1.0'
//    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

 

 

 

1. Service 

HTTP Method(GET/POST/PUT/DELETE)로 API 요청을 위한 interface

 

interface GoogleNewsService {

    @GET("/rss")
    fun getNews(
        @Query("hl") hl: String,
        @Query("gl") gl: String,
        @Query("ceid") ceid: String
    ): Call<RespNewsRss>

}

 

추가적인 사용 방법들은 아래 포스팅에 잘 설명되어 있다.

 

https://medium.com/@ggikko/retrofit-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%93%B0%EA%B8%B0-a6ce90cf7768

 

Retrofit 홈페이지 한글로 긁어보기 RESTFUL SERVICE VERY EASY!!

원래 Volley를 쓰다가 부분적으로 적용해서 사용해 보려했는데 하다보니 홈페이지에 있는 글을 다 읽고 말았다. 그래서 영어로 읽으면 시간이 오래걸려서 한글로 작성해보려한다.. 허접하게 번역

medium.com

 

 

 

 

2. OkHttpClient

API 요청과 응답에 관한 설정을 할 수 있다.

object GoogleNewsServiceFactory

    private const val HTTP_TIMEOUT_READ = 10L
    private const val HTTP_TIMEOUT_CONNECT = 5L

    private fun makeOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor, appendHeaderInterceptor: Interceptor) = OkHttpClient.Builder()
        .connectTimeout(HTTP_TIMEOUT_CONNECT, TimeUnit.MILLISECONDS)
        .readTimeout(HTTP_TIMEOUT_READ, TimeUnit.MILLISECONDS)
        .addInterceptor(httpLoggingInterceptor)
        .addInterceptor(appendHeaderInterceptor)
        .build()

    private fun makeLoggingInterceptor(debug: Boolean): HttpLoggingInterceptor {
        return HttpLoggingInterceptor().apply {
            level = if (debug) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
        }
    }

    private fun makeAppendHeaderInterceptor() = Interceptor { chain ->
            val newRequest = chain.request().newBuilder()
//            newRequest.addHeader("Version", "v1.0")
            chain.proceed(newRequest.build())
    }

 

connectTimeout / readTimeout / writeTimeout / callTimeout

타임아웃을 설정 가능

 

HttpLoggingInterceptor

요청과 응답 로그를 볼 수 있다.

 

 

makeAppendHeaderInterceptor

1번의 Service에서도 각각의 요청마다 Header를 붙일 수 있다.

하지만 디폴트 헤더 데이터가 필요한 경우 interceptor를 만들어 요청마다 같은 헤더를 추가할 수 있다.

 

 

 

 

3. Retrofit을 이용한 Service 구현체

실제 사용하기 위한 구현체 생성

object GoogleNewsServiceFactory

    fun makeNewsService(debug: Boolean, baseUrl: String): GoogleNewsService {
        val okHttpClient = makeOkHttpClient(
            makeLoggingInterceptor(debug),
            makeAppendHeaderInterceptor()
        )
        return makeNewsService(
            baseUrl,
            okHttpClient
        )
    }

    private fun makeNewsService(baseUrl: String, okHttpClient: OkHttpClient): GoogleNewsService {
        val retrofit = Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(okHttpClient)
            .addConverterFactory(SimpleXmlConverterFactory.create())
            .build()

        return retrofit.create(GoogleNewsService::class.java)

 

baseUrl(baseUrl)

기본 URL을 설정

 

client(okHttpClient)

2번에서 만들었던 okHttpClient 설정

 

addConverterFactory(SimpleXmlConverterFactory.create())

응답 데이터를 파싱해 객체로 받기 위한 설정

 

 

 

 

XML

 

dependecies

implementation 'com.squareup.retrofit2:converter-simplexml:2.1.0'

 

Retrofit Builder 

addConverterFactory(SimpleXmlConverterFactory.create())

 

<rss xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
  <channel>
  <generator>NFE/5.0</generator>
  <title>주요 뉴스 - Google 뉴스</title>
  <link>https://news.google.com/?hl=ko&gl=KR&ceid=KR:ko</link>
  <language>ko</language>
  <webMaster>news-webmaster@google.com</webMaster>
  <copyright>2020 Google Inc.</copyright>
  <lastBuildDate>Fri, 29 May 2020 03:09:37 GMT</lastBuildDate>
  <description>Google 뉴스</description>
  <item>
    <title>[속보] 코로나19 확진 58명 늘어…신규 확진자 전원 수도권 - 한겨레</title>
    <link>https://news.google.com/__i/rss/rd/articles/CBMiNWh0dHA6Ly93d3cuaGFuaS5jby5rci9hcnRpL3NvY2lldHkvaGVhbHRoLzk0NzA1Ny5odG1s0gEA?oc=5</link>
    <guid isPermaLink="false">52782408608000</guid>
    <pubDate>Fri, 29 May 2020 01:23:03 GMT</pubDate>
    <description><ol><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiNWh0dHA6Ly93d3cuaGFuaS5jby5rci9hcnRpL3NvY2lldHkvaGVhbHRoLzk0NzA1Ny5odG1s0gEA?oc=5" target="_blank">[속보] 코로나19 확진 58명 늘어…신규 확진자 전원 수도권</a>&nbsp;&nbsp;<font color="#6f6f6f">한겨레</font></li><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9UEFEOE1UVXBoMmfSAQA?oc=5" target="_blank">쿠팡발 감염 확산에 수도권 '비상'…모자·신발서 코로나 나와</a>&nbsp;&nbsp;<font color="#6f6f6f">뉴스TVCHOSUN</font></li><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiMWh0dHBzOi8vd3d3Lnl0bi5jby5rci9fbG4vMDEwM18yMDIwMDUyOTEwNTI0NDc0NDbSAUNodHRwczovL20ueXRuLmNvLmtyL25ld3Nfdmlldy5hbXAucGhwP3BhcmFtPTAxMDNfMjAyMDA1MjkxMDUyNDQ3NDQ2?oc=5" target="_blank">신규 발생 58명...지역 발생 55명 모두 수도권에 집중</a>&nbsp;&nbsp;<font color="#6f6f6f">YTN</font></li><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiJ2h0dHBzOi8vbmV3cy5qb2lucy5jb20vYXJ0aWNsZS8yMzc4ODg4MNIBK2h0dHBzOi8vbW5ld3Muam9pbnMuY29tL2FtcGFydGljbGUvMjM3ODg4ODA?oc=5" target="_blank">코로나 신규환자 58명, 지역감염 55명···모두 수도권서 터졌다 - 중앙일보</a>&nbsp;&nbsp;<font color="#6f6f6f">중앙일보 모바일</font></li><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9cnVjU2VzMVB0R2_SAQA?oc=5" target="_blank">수도권 덮친 '쿠팡 물류센터 집단감염'…"2주가 고비" / JTBC 뉴스룸</a>&nbsp;&nbsp;<font color="#6f6f6f">JTBC News</font></li><li><strong><a href="https://news.google.com/stories/CAAqOQgKIjNDQklTSURvSmMzUnZjbmt0TXpZd1NoTUtFUWlBMHR6bGxZQU1FVmdIQWUwRXpZRzRLQUFQAQ?oc=5" target="_blank">Google 뉴스에서 전체 콘텐츠 보기</a></strong></li></ol></description>
    <source url="http://www.hani.co.kr">한겨레</source>
  </item>

 

@Root(name = "rss", strict = false)
data class RespNewsRss(
    @field:Element(name = "channel") var channel: RespNewsRssChannel? = null
)

@Root(name = "channel", strict = false)
class RespNewsRssChannel(
    @field:ElementList(entry = "item", inline = true) var newsList: List<RespNews>? = null
)

@Element(name = "item")
data class RespNews(
    @field:Element(name = "title") var title: String? = null,
    @field:Element(name = "link") var link: String? = null,
    @field:Element(name = "guid") var guid: String? = null,
    @field:Element(name = "pubDate") var pubDate: String? = null,
    @field:Element(name = "description") var description: String? = null,
    @field:Element(name = "source") var source: String? = null
)

 

 

Json

 

depenencies

implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

 

Retrofit Builder

.addConverterFactory(GsonConverterFactory.create())

 

{
  "rss" : {
    "channel" : {
    	"items" : [{
          "title" : "인천시 “쿠팡물류센터 확진자 딸 등 2명 추가 확진” - 한겨레",
          "link" : "https://news.google.com/__i/rss/rd/articles/CBMiM2h0dHA6Ly93d3cuaGFuaS5jby5rci9hcnRpL2FyZWEvY2FwaXRhbC85NDcyMjIuaHRtbNIBAA?oc=5",
          "guid" : "52782408608000",
          "pubDate" : "Sun, 31 May 2020 05:56:12 GMT",
          "description" : "<ol><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiM2h0dHA6Ly93d3cuaGFuaS5jby5rci9hcnRpL2FyZWEvY2FwaXRhbC85NDcyMjIuaHRtbNIBAA?oc=5" target="_blank">인천시 “쿠팡물류센터 확진자 딸 등 2명 추가 확진”</a>&nbsp;&nbsp;<font color="#6f6f6f">한겨레</font></li><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiOGh0dHBzOi8vd3d3Lmhhbmtvb2tpbGJvLmNvbS9OZXdzL1JlYWQvMjAyMDA1MzAxNjc3MDQ3MzMy0gEA?oc=5" target="_blank">이태원 클럽→쿠팡 물류센터→여의도 학원 전파?</a>&nbsp;&nbsp;<font color="#6f6f6f">한국일보</font></li><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiNWh0dHA6Ly93d3cuaGFuaS5jby5rci9hcnRpL3NvY2lldHkvaGVhbHRoLzk0NzE4OS5odG1s0gEA?oc=5" target="_blank">부천 쿠팡물류센터 관련 코로나19 확진자 최소 109명</a>&nbsp;&nbsp;<font color="#6f6f6f">한겨레</font></li><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiJ2h0dHBzOi8vbmV3cy5qb2lucy5jb20vYXJ0aWNsZS8yMzc4OTYxONIBK2h0dHBzOi8vbW5ld3Muam9pbnMuY29tL2FtcGFydGljbGUvMjM3ODk2MTg?oc=5" target="_blank">쿠팡 물류센터 총 108명 확진…"이태원클럽 학원강사발 추정" - 중앙일보</a>&nbsp;&nbsp;<font color="#6f6f6f">중앙일보 모바일</font></li><li><a href="https://news.google.com/__i/rss/rd/articles/CBMiK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9R1RwQ3ZuQ2I2U1nSAQA?oc=5" target="_blank">코로나19 신규 확진 39명…수도권 이어 지역서도 산발 감염</a>&nbsp;&nbsp;<font color="#6f6f6f">뉴스TVCHOSUN</font></li><li><strong><a href="https://news.google.com/stories/CAAqOQgKIjNDQklTSURvSmMzUnZjbmt0TXpZd1NoTUtFUWlBMHR6bGxZQU1FV1BYSElmd2VCR2RLQUFQAQ?oc=5" target="_blank">Google 뉴스에서 전체 콘텐츠 보기</a></strong></li></ol>",
          "soruce" : "한겨레"
        }]
    }
  }
}

 

data class RespNewsRss(
    @field:SerializedName("channel") var channel: RespNewsRssChannel? = null
)

class RespNewsRssChannel(
    @field:SerializedName("items") var newsList: List<RespNews>? = null
)

data class RespNews(
    @field:SerializedName("title") var title: String? = null,
    @field:SerializedName("link") var link: String? = null,
    @field:SerializedName("guid") var guid: String? = null,
    @field:SerializedName("pubDate") var pubDate: String? = null,
    @field:SerializedName("description") var description: String? = null,
    @field:SerializedName("source") var source: String? = null
)

 

 

 

 

실제 사용

MainActivity

    private val newsService = GoogleNewsServiceFactory.makeNewsService(BuildConfig.DEBUG, "https://news.google.com")

    private fun initViews() {
        btn_main.setOnClickListener {
            newsService.getNews("ko", "KR", "KR:ko")
                .enqueue(object : Callback<RespNewsRss> {
                    override fun onResponse(
                        call: Call<RespNewsRss>,
                        response: Response<RespNewsRss>
                    ) {
                        tv_main.text = response.body()?.channel?.newsList?.toString()
                    }

                    override fun onFailure(call: Call<RespNewsRss>, t: Throwable) {
                        Toast.makeText(this@MainActivity, t.message, Toast.LENGTH_SHORT).show()
                    }
                })
        }
    }

 

 

 

 


Commit ad3ddc26

 

 

+Rx

1. Service

2. OkHttpClient

3. Retrofit을 이용한 Service 구현체

 

 

dependecies 추가

implementation "com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0"

 

 

1. Service

interface GoogleNewsService {

    @GET("/rss")
    fun getNews(
        @Query("hl") hl: String,
        @Query("gl") gl: String,
        @Query("ceid") ceid: String
    ): Single<RespNewsRss>

}

리턴 타입만 바꼈다.

 

 

2. 동일함

 

 

3. Retrofit을 이용한 Service 구현체

    private fun makeNewsService(baseUrl: String, okHttpClient: OkHttpClient): GoogleNewsService {
        val retrofit = Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(okHttpClient)
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(SimpleXmlConverterFactory.create())
            .build()

        return retrofit.create(GoogleNewsService::class.java)
    }

 

addCallAdapterFactory(RxJava2CallAdapterFactory.create())

기존의 리턴받던 Call에서 Rx객체로 변경하기 위한 한줄이 추가되었다.

 

 

실제사용

    private val newsService = GoogleNewsServiceFactory.makeNewsService(BuildConfig.DEBUG, "https://news.google.com")
    private val compositeDisposable = CompositeDisposable()

    private fun initViews() {
        btn_main.setOnClickListener {
            newsService.getNews("ko", "KR", "KR:ko")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { resp ->
                    val newsList = resp.channel?.newsList
                    tv_main.text = newsList.toString()
                }.let {
                    compositeDisposable.add(it)
                }
        }
    }

 

 

 

 

'창고' 카테고리의 다른 글

custom_dialog (+custom_toast)  (0) 2020.05.02
Activity간의 화면 전환  (0) 2020.03.11
internal_storage  (0) 2020.02.26
permission  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09

 

개발포스팅은 언제나 유통기한을 조심하세요.

 

 

Github : https://github.com/junghun9102/AndroidTemplate

Branch : ui/custom_dialog

 

junghun9102/AndroidTemplate

Contribute to junghun9102/AndroidTemplate development by creating an account on GitHub.

github.com

 

Activity 외에 필수적으로 사용하게 되는 Toast, Dialog 등이 있다.

앱 디자인에 더 완벽하게 맞추고 싶을 수 있다.

 

커스텀해서 사용해보자!

 

 

 


CustomToast

 

 

 

 

view_toast.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_view_toast"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp"
        android:padding="15dp"
        android:background="#7000"
        android:gravity="center"
        android:textColor="@android:color/white"/>

</LinearLayout>

 

원하는 토스트 레이아웃을 만든다

 

 

 

CustomToast.kt

object CustomToast {

    fun show(context: Context, stringResId: Int, duration: Int = Toast.LENGTH_SHORT) = show(context, context.getString(stringResId), duration)

    private fun show(context: Context, content: String, duration: Int = Toast.LENGTH_SHORT) {
        val view = LayoutInflater.from(context).inflate(R.layout.view_toast, null)
        view.tv_view_toast.text = content

        Toast(context).apply {
            setGravity(
                Gravity.FILL_HORIZONTAL or Gravity.BOTTOM,
                0,
                context.dimen(R.dimen.custom_toast_bottom_margin)
            )
            this.duration = duration
            setView(view)
        }.show()
    }

}

 

dimen에 bottomMargin을 정해두고 사용했다.

(혹시 모르시는 분이 있을까봐 context.dimen은 anko 라이브러리)

 

토스트 내용은 하드코딩하지 않는 한 resource 레퍼런스만

받아서 사용하기 때문에 String으로 받는 함수는 private으로 묶어 두었다.

 

 

 

 

ContextExtensions.kt

fun Context.showToast(resId: Int) = CustomToast.show(this, resId)

 

MainActivity.kt

    private fun initViews() {
        btn_main_toast.setOnClickListener {
            showToast(R.string.app_name)
        }
    }

 

사용하기 쉽도록 extension을 추가했다.

 

 

 


CustomDialog

 

 

 

 

원하는 레이아웃형태 만들기

view_dialog.xml

더보기
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <TextView
        android:id="@+id/tv_view_dialog_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textSize="20dp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tv_view_dialog_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="20dp"
        android:gravity="end">

        <TextView
            android:id="@+id/tv_view_dialog_negative"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:padding="5dp"
            android:foreground="?attr/selectableItemBackground"
            android:text="@string/common_cancel" />

        <TextView
            android:id="@+id/tv_view_dialog_positive"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:foreground="?attr/selectableItemBackground"
            android:text="@string/common_confirm" />

    </LinearLayout>
    
</LinearLayout>

 

 

 

 

CustomDialog.kt

class CustomDialog(
    context: Context,
    title: String,
    content: String? = null
) {

    private val view = LayoutInflater.from(context).inflate(R.layout.view_dialog, null)

    private val dialog = AlertDialog.Builder(context)
        .setView(view)
        .create()

    fun show() {
        dialog.show()
    }
    
    fun setPositiveButton(buttonTextResId: Int, func: () -> Unit = defaultClickFunc) =
        setPositiveButton(view.context.getString(buttonTextResId), func)
    
    /*
        이하 코드생략
    */
    
}

 

전체코드 보기

더보기
class CustomDialog(
    context: Context,
    title: String,
    content: String? = null
) {

    constructor(context: Context, titleResId: Int, contentResId: Int? = null) : this(
        context,
        context.getString(titleResId),
        contentResId?.let { context.getString(it) }
    )

    private val view = LayoutInflater.from(context).inflate(R.layout.view_dialog, null)
    private val titleView = view.tv_view_dialog_title
    private val contentView = view.tv_view_dialog_content
    private val positiveView = view.tv_view_dialog_positive
    private val negativeView = view.tv_view_dialog_negative
    private val defaultClickFunc = { dialog.dismiss() }

    init {
        titleView.text = title
        content?.let {
            contentView.text = it
        } ?: kotlin.run {
            contentView.visibility = View.GONE
        }

        positiveView.setOnClickListener { defaultClickFunc.invoke() }
        negativeView.setOnClickListener { defaultClickFunc.invoke() }
    }

    private val dialog = AlertDialog.Builder(context)
        .setView(view)
        .create()

    fun show() {
        dialog.show()
    }

    fun setPositiveButton(buttonText: String, func: () -> Unit = defaultClickFunc): CustomDialog {
        setTextViewForButton(positiveView, buttonText, func)
        return this
    }

    fun setPositiveButton(buttonTextResId: Int, func: () -> Unit = defaultClickFunc) =
        setPositiveButton(view.context.getString(buttonTextResId), func)

    fun setNegativeButton(buttonText: String, func: () -> Unit = defaultClickFunc): CustomDialog {
        setTextViewForButton(negativeView, buttonText, func)
        return this
    }

    fun setNegativeButton(buttonTextResId: Int, func: () -> Unit = defaultClickFunc) =
        setNegativeButton(view.context.getString(buttonTextResId), func)

    private fun setTextViewForButton(view: TextView, buttonText: String, func: () -> Unit) {
        view.run {
            text = buttonText
            setOnClickListener {
                func.invoke()
                dialog.dismiss()
            }
        }
    }

}

 

 

MainActivity.kt

    private fun initViews() {
        btn_main_alert_dialog.setOnClickListener {
            CustomDialog(this, R.string.dialog_delete_title, R.string.dialog_delete_content)
                .setPositiveButton(R.string.common_delete) {
                    showToast(R.string.message_delete_complete)
                }
                .show()
        }
    }

 

핵심은 인플레이팅한 커스텀 뷰를 AlertDialog에 setView하는 것이다.

 

기존의 AlertDialog를 대체할 Dialog를 만들었지만

전혀 다른 방식으로 간단한 선택창 또는 도움창 등을 보여주는데 사용할 수 있다.

 

 

 

 

 

'창고' 카테고리의 다른 글

Retrofit / OkHttp  (0) 2020.05.29
Activity간의 화면 전환  (0) 2020.03.11
internal_storage  (0) 2020.02.26
permission  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09

 

 

overridePendingTransition(int enterAnim, int exitAnim)

Call immediately after one of the flavors of startActivity(android.content.Intent) or finish() to specify an explicit transition animation to perform next.

 

 

1. Activity가 실행, 종료 코드 직후 바로 호출하면된다.

2. 첫번째 파라미터로 보여질 화면의 애니메이션을 입력한다.

3. 두번째 파라미터로 종료되거나 가려질 화면의 애니메이션을 입력한다.

 

 

 

 


 

MainActivity의 왼쪽에서 LeftActivity가 들어온다.

 

 

MainActivity.kt

        btn_main_left.setOnClickListener {
            startActivity(Intent(this@MainActivity, LeftActivity::class.java))
            overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
        }

 

1. startActivity 후에 overridePendingTransition을 호출한다.

2. LeftActivity는 왼쪽에서 들어와야 하기 때문에 slide_in_left를 넘겨준다.

3. MainActivity는 오른쪽으로 나가야 하기 때문에 slide_out_right를 넘겨준다.

 

 

 

slide_in_left.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

 

slide_out_right.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="100%p"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

 

 

 

 

 


 

종료시에는 반대로

MainActivity가 오른쪽에서 들어오고 LeftActivity가 왼쪽으로 나가는 것이 자연스럽다.

 

 

LeftActivity.kt

    override fun finish() {
        super.finish()
        overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)
    }

 

1. finish 후에 overridePendingTransition을 호출한다.

2. MainActivity는 오른쪽에서 들어와야 하기 때문에 slide_in_right를 넘겨준다.

3. LeftActivity는 왼쪽으로 나가야 하기 때문에 slide_out_left를 넘겨준다.

 

 

 

slide_in_right.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

 

slide_out_left.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="-100%p"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

 

 

 

 

 


 

위에 예시를 포함한

상황에 맞는 애니메이션을 사용하면 된다.

 

fromXDelta, toXDelta를 fromYDelta, toTDelta로 변경하면 위아래로 이동할 수 있고

android.R.anim.fade_in, android.R.anim.fade_out을 사용할 수도 있다.

 

 

 

 

 

'창고' 카테고리의 다른 글

Retrofit / OkHttp  (0) 2020.05.29
custom_dialog (+custom_toast)  (0) 2020.05.02
internal_storage  (0) 2020.02.26
permission  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09

개발포스팅은 언제나 유통기한을 조심하세요.

 

 

Github : https://github.com/junghun9102/AndroidTemplate

Branch : base/internal_storage

 

junghun9102/AndroidTemplate

Contribute to junghun9102/AndroidTemplate development by creating an account on GitHub.

github.com

 

내부저장소와 외부저장소는 앱의 내부냐 외부냐의 차이일 뿐이다.

 

Android 10이 나오면서 외부저장소에 대한 보안이 강화되었다.

사실 대부분의 앱들이 굳이 외부저장소가 필요한가 싶다.

 

내부저장소를 사용하는 branch를 완성하기는 했는데 만들고 나니

별내용이 없는데 이걸 왜 굳이 작성했을까 하는 생각이 들었다.

 

사실 Context.filesDir.path에서 받아오는 내부저장소의 rootPath가 전부이다.

나머지는 파일을 이용하는 작업일 뿐이다.

 

내부저장소를 이용해 카메라 사진을 저장할 때 FileProvider를 사용하면 된다.

 

 

 

 

 


핵심 코드

 

MainActivity

    private val internalStoragePath: String by lazy { filesDir.path }
    private val saveFileDirectory = "/textFolder"

    private val saveStoragePath: String by lazy { "$internalStoragePath$saveFileDirectory" }
    private val fileName = "textFileName.txt"

 

 

 

 

 


부가 코드

 

데이터 로드

            // 1
            val savedNoteFile = File(saveStoragePath, fileName)
            val note = FileManager.getTextFromFile(savedNoteFile)
            showNote(note)

//            // 2
//            val inputStream = openFileInput(fileName)
//            val note = FileManager.getTextFromInputStream(inputStream)
//            inputStream.close()
//            showNote(note)

 

데이터 저장

            // 1
            val file = File(saveStoragePath, fileName)
            val textToSave = et_main_note.text.toString()
            FileManager.writeTextToFile(file, textToSave)

//            // 2
//            val outputStream = openFileOutput(fileName, Context.MODE_PRIVATE)
//            val textToSave = et_main_note.text.toString()
//            FileManager.writeTextToOutputStream(outputStream, textToSave)
//            outputStream.close()

 

주석처리해 놓은 코드와 같이 파일을 거치지 않고 바로 stream으로 받아올 수 있다.

단, 내부저장소의 기본 저장소를 사용해야만 하는 것 같다. 

 

 

FileManeger

object FileManager {

    @Throws(IOException::class)
    fun getTextFromFile(file: File): String {
        val inputStream = file.inputStream()
        val text = getTextFromInputStream(inputStream)
        inputStream.close()

        return text
    }

    @Throws(IOException::class)
    fun getTextFromInputStream(inputStream: InputStream): String {
        val br = inputStream.bufferedReader()
        val text = br.useLines { lines ->
            lines.fold("") { sum, line ->
                sum + if (sum.isNotBlank()) ", $line" else line
            }
        }

        br.close()
        return text
    }

    @Throws(IOException::class)
    fun writeTextToFile(file: File, text: String) {
        val outputStream = file.outputStream()
        writeTextToOutputStream(outputStream, text)
        outputStream.close()
    }

    @Throws(IOException::class)
    fun writeTextToOutputStream(outputStream: OutputStream, text: String) {
        val bw = outputStream.bufferedWriter()
        bw.write(text)
        bw.close()
    }

}

 

 

 

 

 

'창고' 카테고리의 다른 글

custom_dialog (+custom_toast)  (0) 2020.05.02
Activity간의 화면 전환  (0) 2020.03.11
permission  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09
프로그래밍 용어 정리  (0) 2019.05.27

개발포스팅은 언제나 유통기한을 조심하세요.

 

 

Github : https://github.com/junghun9102/AndroidTemplate

Branch : base/permission

 

junghun9102/AndroidTemplate

Contribute to junghun9102/AndroidTemplate development by creating an account on GitHub.

github.com

 

 

권한이 필요한 기능에 대한 동작 정의

 

1. Manifest에 권한을 추가한다.

2. 권한이 필요한 기능을 요청한다.

3-1. 권한이 있다면 그대로 기능을 실행 -> FIN

3-2. 권한이 없다면 권한 요청 -> 4

4-1. 권한 요청 수락하면 기능 실행

4-2. 권한 요청 거절하면 메시지 표시

 

 

 

전체코드 먼저 보기

더보기
object PermissionHelper {

    const val PERMISSIONS_CAMERA = 1

    private val permissionList = mapOf(
        PERMISSIONS_CAMERA to arrayOf(android.Manifest.permission.CAMERA)
    )

    private fun getPermissions(requestCode: Int) = permissionList[requestCode] ?: throw IllegalArgumentException("You must use requestCode defined at PermissionHelper")

    fun checkPermission(context: Context, requestCode: Int): Boolean {
        val permissions = getPermissions(requestCode)
        return checkPermissions(context, permissions)
    }

    private fun checkPermission(context: Context, permission: String): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }

    private fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
        return permissions.fold(true, { acc, permission ->
            acc && checkPermission(context, permission)
        })
    }

    fun requestPermissions(activity: Activity, requestCode: Int) {
        val permissions = getPermissions(requestCode)
        ActivityCompat.requestPermissions(activity, permissions, requestCode)
    }

    fun onRequestPermissionResult(context: Context, requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) {
        val permissions = getPermissions(requestCode)
        if (checkPermissions(context, permissions)) {
            funcAllowed.invoke()
        } else {
            funcNotAllowed.invoke()
        }
    }
}

// PermissionHelper Extensions
fun Activity.requestPermissions(requestCode: Int) = PermissionHelper.requestPermissions(this, requestCode)

fun Context.checkPermissions(requestCode: Int) = PermissionHelper.checkPermission(this, requestCode)

fun Context.onRequestPermissionResult(requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) =
    PermissionHelper.onRequestPermissionResult(this, requestCode, funcAllowed, funcNotAllowed)

fun Activity.checkPermissionsAndDoFunctionOrRequest(requestCode: Int, funcAllowed: () -> Unit) {
    if (this.checkPermissions(requestCode))
        funcAllowed.invoke()
    else
        this.requestPermissions(requestCode)
}

 

 

 


필요한 권한 추가 (1)

 

AndroidManifest.xml

    <uses-permission android:name="android.permission.CAMERA"/>

 

권한 정리

    const val PERMISSIONS_CAMERA = 1

    private val permissionList = mapOf(
        PERMISSIONS_CAMERA to arrayOf(android.Manifest.permission.CAMERA)
    )

 

기능별로 필요한 권한들을 정리해둔다.

 

 

 

 

 


기능 요청 (2 ~ 3)

 

MainActivity

    private fun initViews() {
        btn_main_camera.setOnClickListener {
            checkPermissionAndStartCamera()
        }
    }

    private fun checkPermissionAndStartCamera() {
        checkPermissionsAndDoFunctionOrRequest(PermissionHelper.PERMISSIONS_CAMERA) {
            startCamera()
        }
    }

    private fun startCamera() {
        Toast.makeText(this, "카메라를 시작합니다 위이잉", Toast.LENGTH_SHORT).show()
    }

 

권한이 있다면 바로 기능 수행

 

 

extensions

fun Activity.requestPermissions(requestCode: Int) = PermissionHelper.requestPermissions(this, requestCode)

fun Context.checkPermissions(requestCode: Int) = PermissionHelper.checkPermission(this, requestCode)

fun Activity.checkPermissionsAndDoFunctionOrRequest(requestCode: Int, funcAllowed: () -> Unit) {
    if (this.checkPermissions(requestCode))
        funcAllowed.invoke()
    else
        this.requestPermissions(requestCode)
}

 

Activity.checkPermissionsAndDoFunctionOrRequest에서 퍼미션을 체크하고

모두 권한이 주어져있다면 funcAllowed가 아니라면 requestPermissions 수행

 

 

PermissionHelper

object PermissionHelper {

    const val PERMISSIONS_CAMERA = 1

    private val permissionList = mapOf(
        PERMISSIONS_CAMERA to arrayOf(android.Manifest.permission.CAMERA)
    )

    private fun getPermissions(requestCode: Int) = permissionList[requestCode] ?: throw IllegalArgumentException("You must use requestCode defined at PermissionHelper")

    fun checkPermission(context: Context, requestCode: Int): Boolean {
        val permissions = getPermissions(requestCode)
        return checkPermissions(context, permissions)
    }

    private fun checkPermission(context: Context, permission: String): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }

    private fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
        return permissions.fold(true, { acc, permission ->
            acc && checkPermission(context, permission)
        })
    }

    fun requestPermissions(activity: Activity, requestCode: Int) {
        val permissions = getPermissions(requestCode)
        ActivityCompat.requestPermissions(activity, permissions, requestCode)
    }
    
    ...
    
}

 

코드가 길어 읽기 싫지만 별거없는 코드다.

 

 

 

 

 


권한 요청에 따른 반응 (4)

 

MainActivity

	override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        when (requestCode) {
            PermissionHelper.PERMISSIONS_CAMERA ->
                onRequestPermissionResult(PermissionHelper.PERMISSIONS_CAMERA, {
                    startCamera()
                }) {
                    showPermissionDeniedMessage()
                }
        }
    }

    private fun showPermissionDeniedMessage() {
        Toast.makeText(this, "권한을 허용하지 않아 카메라를 사용할 수 없습니다.", Toast.LENGTH_SHORT).show()
    }

 

권한 요청에 응답을 한 뒤에

다시 요청한 권한리스트를 확인하고 이에 따라 기능을 수행하거나 거부했을 때의 기능을 수행한다.

절대적으로 권한을 승락하게 하고 싶다면 Activity.checkPermissionsAndDoFunctionOrRequest를 사용하면 거절시 다시 요청하게 된다.

 

 

PermissionHelper

    fun onRequestPermissionResult(context: Context, requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) {
        val permissions = getPermissions(requestCode)
        if (checkPermissions(context, permissions)) {
            funcAllowed.invoke()
        } else {
            funcNotAllowed.invoke()
        }
    }

 

퍼미션에 관한 코드를 캡슐화함으로 MainActivity의 코드가 많이 줄었다.

권한과 관련된 유명한 TedPermission도 있지만 직접 내 코드를 만들고 싶었다.

 

 

 

 

 


전체 코드

 

object PermissionHelper {

    const val PERMISSIONS_CAMERA = 1

    private val permissionList = mapOf(
        PERMISSIONS_CAMERA to arrayOf(android.Manifest.permission.CAMERA)
    )

    private fun getPermissions(requestCode: Int) = permissionList[requestCode] ?: throw IllegalArgumentException("You must use requestCode defined at PermissionHelper")

    fun checkPermission(context: Context, requestCode: Int): Boolean {
        val permissions = getPermissions(requestCode)
        return checkPermissions(context, permissions)
    }

    private fun checkPermission(context: Context, permission: String): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }

    private fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
        return permissions.fold(true, { acc, permission ->
            acc && checkPermission(context, permission)
        })
    }

    fun requestPermissions(activity: Activity, requestCode: Int) {
        val permissions = getPermissions(requestCode)
        ActivityCompat.requestPermissions(activity, permissions, requestCode)
    }

    fun onRequestPermissionResult(context: Context, requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) {
        val permissions = getPermissions(requestCode)
        if (checkPermissions(context, permissions)) {
            funcAllowed.invoke()
        } else {
            funcNotAllowed.invoke()
        }
    }
}

// PermissionHelper Extensions
fun Activity.requestPermissions(requestCode: Int) = PermissionHelper.requestPermissions(this, requestCode)

fun Context.checkPermissions(requestCode: Int) = PermissionHelper.checkPermission(this, requestCode)

fun Context.onRequestPermissionResult(requestCode: Int, funcAllowed: () -> Unit, funcNotAllowed: () -> Unit = {}) =
    PermissionHelper.onRequestPermissionResult(this, requestCode, funcAllowed, funcNotAllowed)

fun Activity.checkPermissionsAndDoFunctionOrRequest(requestCode: Int, funcAllowed: () -> Unit) {
    if (this.checkPermissions(requestCode))
        funcAllowed.invoke()
    else
        this.requestPermissions(requestCode)
}

 

 

 

 

 

 

 

 

 

 

'창고' 카테고리의 다른 글

Activity간의 화면 전환  (0) 2020.03.11
internal_storage  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09
프로그래밍 용어 정리  (0) 2019.05.27
xml로 도형 그리기  (0) 2019.05.26

개발 포스팅은 언제나 유통기한에 주의하세요

 

 

DPI(Dot Per Inch)

 : 1인치당 점(pixel)의 개수

 

ldpi : 120dpi
mdpi : 160dpi
hdpi : 240dpi
xhdpi : 320dpi
xxhdpi : 480dpi
xxxhdpi : 640dpi

 

아래로 갈수록 인치에 들어가는 픽셀의 수가 높다.

즉 고화질을 의미한다.

 

 

여기서 생기는 문제점이 이미지를 px 단위로 가운데 놓았을 때

스마트폰이 점점 발전하여 dpi가 올라간다면

 

 

 

와 같이 동일한 크기를 표현하지 못하게 된다.

 

 

 

 

이를 해결하기 위해서 DP 개념이 존재한다.

 

 

 

 

DP(Device Independence Pixel)

 : 디바이스 독립 픽셀

다양한 DPI에 맞게 변환될 추상 픽셀이라고 하는 거 같습니다.

 

mdpi를 기준으로

DPI가 커질수록 픽셀 값을 비례로 늘려 언제나 같은 크기를 보여주게 됩니다.

 

ldpi : 120dpi   1dp = 0.75px
mdpi : 160dpi(default)   1dp = 1px
hdpi : 240dpi   1dp = 1.5px
xhdpi : 320dpi   1dp = 2px
xxhdpi : 480dpi   1dp = 3px
xxxhdpi : 640dpi   1dp = 4px

 

 

 

 

 

그렇다면 이제 DP를 사용하면 끝인가?

모든 스마트폰에 같은 레이아웃을 사용하면 될까?

 

아니다. 스마트폰마다 다른 DP를 가지고 있기 때문에

태블릿이 아닌 스마트폰만을 위해 개발하더라도 DP에 따른 레이아웃을 만들어야 한다. 

 

 

 

 

 

스마트폰 별 DP

 

레이아웃을 만들 때

세로의 경우 내용이 많은 경우 스크롤이 가능하게 만들기 때문에

가로에 대해서 집중하게 되는 것 같다.

 

세로의 경우에도 고정 DP와 가변적인 부분을 가지게 만든다면 아무런 문제가 없다.

 

하지만 모든 레이아웃에 디자이너 혹은 개발자가 가변적인 부분을 고려하는 것은

쉽지만은 않은 작업이라고 생각한다.

 

그렇기 때문에 다수의 폰이 가지고 있는 넓이인 360dp로 작업을 하고 예외적인 폰에 대응하는 것이 좋아 보인다.

 

 

 

예시로

 

갤럭시 S3(xhdpi) 9:16

720 x 1280

360dp x 640dp

 

갤럭시 S5(xxhdpi) 9:16

1080 × 1920

360dp x 640dp

 

갤럭시 S6(xxxhdpi) 9:16

1440 x 2560

360 x 640dp

 

갤럭시 S1(hdpi) 3:5

480 × 800

320dp x 533dp

 

넥서스4(xhdpi) 3:5

768 x 1280

364dp x 640dp

 

와 같은 스펙들을 가지고 있다

 

옛날 폰들이지만 갤S1과 넥서스4의 경우

360dp에 맞게 고정DP로만 레이아웃을 작성한다면

의도하지 않은 화면을 보여주게 될 것이다.

 

 

 

 

이 곳에 가면 스마트폰 별 해상도를 확인할 수 있다.

https://zetawiki.com/wiki/%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%8F%B0_%ED%95%B4%EC%83%81%EB%8F%84,_%ED%99%94%EB%A9%B4%EB%B9%84%EC%9C%A8

 

스마트폰 해상도, 화면비율 - 제타위키

9:16 ★★★ 1440 × 2560(갤6, 갤7, 갤노4, 갤노5, G3, G4, G5, 픽셀XL)1080 × 1920(갤4, 갤5, 갤노3, 옵G프로, G2, 넥5, 아이폰6, 픽셀)

zetawiki.com

 

물론 저 또한 예외적인 레이아웃에 대해 처리해본 적이 없지만

아래 링크에 포스팅에 이와 관련한 내용을 가지고 있으니 참고하시면 좋을 것 같네요.

https://re-build.tistory.com/34

 

[Android] 해상도별 레이아웃 대응에 대한 고찰

이번에는 여러가지 해상도를 가진 기기에 어떻게 하면 손쉽고 편하게 대응이 가능할 지에 대한 개인적인 고찰을 해보려 합니다. 혹시 더 좋은 방법을 아시는 분이나 아이디어가 있으신 분은 댓글로 알려주신다면..

re-build.tistory.com

 

'창고' 카테고리의 다른 글

internal_storage  (0) 2020.02.26
permission  (0) 2020.02.26
프로그래밍 용어 정리  (0) 2019.05.27
xml로 도형 그리기  (0) 2019.05.26
2019 드로이드 나이츠  (0) 2019.04.26

 

찾아보고 참조하거나 제가 이해하는데로 짧게 정리했습니다

명확한 이해를 위해 추가로 더 찾아보세요

틀린 부분이 있으면 바로 잡아주셔도 좋아요

 


 

라이브러리

 : 자주 사용하고 공통되게 사용할 수 있는 기능들을 묶은 것

 

API(Application Programming Interface)

 : 프로그램을 작성하기 위한 일련의 부 프로그램, 프로토콜 등을 정의하여 상호 작용을 하기 위한 인터페이스 사양

ex) JAVA API, 구글 지도API, 날씨 정보 API, 거래소 API

 

프레임워크

 : 프로그래밍에 집중하도록 기본적인 기능을 제공하는 환경

ex) Spring, Django, Bootstrap, .Net Framework

 

모듈

 :  한 프로그램의 일부분

 

패키지

 : 폴더

ex) ui.main 패키지 안에 메인 화면에 관련된 프로그래밍 파일들이 존재한다

 

아키텍쳐

 : 프로그램에 대한 설계

고객의 현재의 요구사항과 미래의 요구사항을 모두 충족시킬수 있도록 고려하여 설계해야 함

 

플랫폼

 : 프로그램이 실행되는 환경

ex) Windows, Linux, macOS, 앱스토어, 구글플레이

 

컴포넌트

 : 교체되어도 원활하게 동작할 수 있도록 설계한 소프트웨어 부품

ex) 안드로이드 4대 컴포넌트(Service, Intent, Broadcast Receiver, Content Provider)

는 교체되어도 되나? 그냥 객체화가 잘되어 있는 클래스를 뜻하는 건가?

 

IDE(Integrated Development Environment)

ex) AndroidStudio, Eclipse, Xcode

 

SDK(Software Development Kit)

 : 개발 도구의 집합

 

ANR(Application Not Responding)

 : Android의 main thread를 너무 오래 사용해 UI에 대한 처리를 못하는 경우 발생

 

디자인 패턴

 : 설계를 할 때 자주 발생하는 문제를 피하기 위한 패턴

ex) MVC, MVP, MVVM, Repository, DI, Observer

 

애자일

 : 아무런 계획이 없는 개발 방법과 지나치케 계획이 많은 개발 방법들 사이에서 타협접을 찾고자 하는 개발 방법론

 

TDD(Test Driven Development)

 : 요구사항을 검증하는 자동화된 테스트 케이스를 작성하고 이를 통과하기 위한 최소한의 코드를 생성

작성한 코드를 표준에 맞게 리팩토링한다.

 

CI(Continuos Integration)

 : 새로운 코드 변경 사항이 정기적으로 빌드 및 테스트되어 공유 리파지토리에 병합하여 안정적인 개발을 돕는 자동화 프로세스

 

CD(Continuos Delivery)

 : 애플리케이션을 실시간 프로덕션 환경에 적용할 수 있도록 하는 자동화 프로세스

 

DI(Dependency Injection)

 : 외부에서 의존 객체를 생성하여 넘겨주는 것

 

보일러 플레이트 코드

 : 꼭 필요한 기능이지만 코드가 쓸데없이 길고 자주 쓰이는 코드

ex) setter, getter

 

레거시(Legacy)
 : 현재까지 남아 사용되고 있거나 현제의 체계에 영향을 미치는 과거의 체계

 

DTO(Data Transtfer Object)

 : 데이터 전송 객체

 

DAO(Data Access Object)

 : 데이터베이스에 접근하기 위한 클래스

 

쇼트서킷

 : 

 

 

'창고' 카테고리의 다른 글

permission  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09
xml로 도형 그리기  (0) 2019.05.26
2019 드로이드 나이츠  (0) 2019.04.26
드로이드나이츠(Droid Knights) 2018  (0) 2018.06.10

개발 포스팅은 언제나 유통기한에 주의하세요

 


 

https://developer.android.com/guide/topics/resources/drawable-resource?hl=ko#Shape

 

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape=["rectangle" | "oval" | "line" | "ring"] >
    <corners
        android:radius="integer"
        android:topLeftRadius="integer"
        android:topRightRadius="integer"
        android:bottomLeftRadius="integer"
        android:bottomRightRadius="integer" />
    <gradient
        android:angle="integer"
        android:centerX="float"
        android:centerY="float"
        android:centerColor="integer"
        android:endColor="color"
        android:gradientRadius="integer"
        android:startColor="color"
        android:type=["linear" | "radial" | "sweep"]
        android:useLevel=["true" | "false"] />
    <padding
        android:left="integer"
        android:top="integer"
        android:right="integer"
        android:bottom="integer" />
    <size
        android:width="integer"
        android:height="integer" />
    <solid
        android:color="color" />
    <stroke
        android:width="integer"
        android:color="color"
        android:dashWidth="integer"
        android:dashGap="integer" />
</shape>

 

shape : 모양, default=rectangle

corners : 모서리의 둥근 정도

gradient : 그라데이션이 들어간 배경

padding : 패딩

size : 사이즈

solid : 배경색

stroke : 외곽선

 


 

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <size android:width="100dp"
        android:height="100dp" />

    <solid android:color="@color/colorPrimary" />

    <stroke android:color="@color/colorAccent"
        android:width="10dp"/>

    <corners android:radius="0dp" />

</shape>

 


 

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <size android:width="100dp"
        android:height="100dp" />

    <solid android:color="@color/colorPrimary" />

    <stroke android:color="@color/colorAccent"
        android:width="10dp"/>

    <corners android:radius="10dp" />

</shape>

 


 

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <size android:width="100dp"
        android:height="100dp" />

    <gradient
        android:angle="90"
        android:startColor="@color/colorPrimary"
        android:endColor="@android:color/black" />

    <stroke android:color="@color/colorAccent"
        android:width="10dp"/>

    <corners android:radius="10dp" />

</shape>

 


 

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <size android:width="100dp"
        android:height="100dp" />

    <gradient
        android:angle="90"
        android:startColor="@color/colorPrimary"
        android:endColor="@android:color/black" />

    <stroke android:color="@color/colorAccent"
        android:width="10dp"
        android:dashWidth="10dp"
        android:dashGap="1dp"/>

</shape>

 


 

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="ring"
    android:innerRadius="25dp"
    android:thickness="25dp"
    android:useLevel="false">

    <size android:width="100dp"
        android:height="100dp" />

    <solid android:color="@color/colorPrimary" />

</shape>

 

 


 

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="ring"
    android:innerRadius="40dp"
    android:thickness="10dp"
    android:useLevel="false">

    <size android:width="100dp"
        android:height="100dp" />

    <solid android:color="@color/colorPrimary" />

</shape>

 

 

Tistory 코드 블록을 썼는데 글쓸때는 제대로 표시하고 완료된 글은 깨지는건 무엇?

 

'창고' 카테고리의 다른 글

DP(Device Independence Pixel)?  (0) 2019.06.09
프로그래밍 용어 정리  (0) 2019.05.27
2019 드로이드 나이츠  (0) 2019.04.26
드로이드나이츠(Droid Knights) 2018  (0) 2018.06.10
Googl I/O 2017 Extended in Seoul  (0) 2017.11.10

몬감 ㅠㅠ

 

수요일날 100명 남았길래

 

금요일날 신청할랬드만

 

자리있을때 바로 하세요 ㅠㅠ

 

코틀린 나이츠도 못가고...

'창고' 카테고리의 다른 글

프로그래밍 용어 정리  (0) 2019.05.27
xml로 도형 그리기  (0) 2019.05.26
드로이드나이츠(Droid Knights) 2018  (0) 2018.06.10
Googl I/O 2017 Extended in Seoul  (0) 2017.11.10
드로이드나이츠(Droid Knights) 내용  (0) 2017.11.09




두번째 드로이드나이츠입니다



드로이드나이츠 2018

https://droidknights.github.io/2018/


드로이드나이츠 YouTube

2018년 발표 동영상이 업로드 되어 있습니다.

https://www.youtube.com/channel/UCjeUnwS8mHhsl600-nFJKmw





스케줄











설문조사


항상 설문조사가 있어요


012345







현장사진









굿즈


좋아요






'창고' 카테고리의 다른 글

xml로 도형 그리기  (0) 2019.05.26
2019 드로이드 나이츠  (0) 2019.04.26
Googl I/O 2017 Extended in Seoul  (0) 2017.11.10
드로이드나이츠(Droid Knights) 내용  (0) 2017.11.09
드로이드나이츠(Droid Knights)  (0) 2017.11.09

+ Recent posts