알파채널 개념을 개발한 토마스 포터와 톰 더프의 이름을 따서 만들었다.

 

대상(렌더링 대상의 콘텐츠)을 사용하여

소스(렌더 할 그래픽 개체)의 합성 결과 색상을 계산하는 방법으로

12개의 합성 연산자로 구성되어 있다.

 

 

 

 

 

구현해보기

 

        private fun drawPorterDuffSample(canvas: Canvas) {
            val sampleBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
            val sampleCanvas = Canvas(sampleBitmap)
 
            sampleCanvas.drawBitmap(getCircleBitmap(), 0f, 0f, Paint())
            sampleCanvas.drawBitmap(getRectangleBitmap(), 0f, 0f, Paint().apply {
                xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
            })
 
            canvas.drawBitmap(sampleBitmap, 0f, 0f, Paint())
        }
cs

 

 

canvas의 drawRect, drawCircle과 같은 메소드에 적용하면 기대와 다른 결과가 나온다.

SRC, DST 모두 Bitmap으로 사용해야 기대한 결과를 볼 수 있었다.

 

위 코드에서 rectangleBitmap이 SRC, circleBitmap이 DST가 된다.

  

 

전체 코드

더보기
    class Renderer {
        private var width: Int = 0
        private var height: Int = 0
        private var centerX: Int = 0
        private var centerY: Int = 0
 
        fun setWidthAndHeight(width: Int, height: Int) {
            this.width = width
            this.height = height
            centerX = width/2
            centerY = height/2
        }
 
        fun onDraw(canvas: Canvas) {
            drawPorterDuffSample(canvas)
        }
 
        private fun drawPorterDuffSample(canvas: Canvas) {
            val sampleBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
            val sampleCanvas = Canvas(sampleBitmap)
 
            sampleCanvas.drawBitmap(getCircleBitmap(), 0f, 0f, Paint())
            sampleCanvas.drawBitmap(getRectangleBitmap(), 0f, 0f, Paint().apply {
                xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
            })
 
            canvas.drawBitmap(sampleBitmap, 0f, 0f, Paint())
        }
 
        private fun getCircleBitmap() = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
            val canvas = Canvas(this)
            val paint = Paint().apply {
                flags = Paint.ANTI_ALIAS_FLAG
                color = Color.parseColor("#E9B639")
            }
            val radius = width*3.5f/10
 
            canvas.drawCircle(radius, radius, radius, paint)
        }
 
        private fun getRectangleBitmap() = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
            val canvas = Canvas(this)
            val paint = Paint().apply {
                flags = Paint.ANTI_ALIAS_FLAG
                color = Color.parseColor("#4899C5")
            }
            val startTop = width*3/10
            val endBottom = width*9/10
 
            canvas.drawRect(
                Rect(startTop, startTop, endBottom, endBottom),
                paint
            )
        }
 
    }
cs

 

 

 

 

 

 

결과

 

 

위에 코드를 안드로이드 커스텀뷰에서 그린 결과이다.

 

안드로이드 커스텀뷰 코드

더보기
class CustomView(context: Context, attrs: AttributeSet?, defStyle: Int) : View(context, attrs, defStyle) {
    constructor(context: Context) : this(context, null0)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
 
    private val renderer = Renderer()
 
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        renderer.setWidthAndHeight(measuredWidth, measuredHeight)
    }
 
    override fun onDraw(canvas: Canvas) {
        renderer.onDraw(canvas)
    }
 
    class Renderer {
        private var width: Int = 0
        private var height: Int = 0
        private var centerX: Int = 0
        private var centerY: Int = 0
 
        fun setWidthAndHeight(width: Int, height: Int) {
            this.width = width
            this.height = height
            centerX = width/2
            centerY = height/2
        }
 
        fun onDraw(canvas: Canvas) {
            drawPorterDuffSample(canvas)
        }
 
        private fun drawPorterDuffSample(canvas: Canvas) {
            val sampleBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
            val sampleCanvas = Canvas(sampleBitmap)
 
            sampleCanvas.drawBitmap(getCircleBitmap(), 0f, 0f, Paint())
            sampleCanvas.drawBitmap(getRectangleBitmap(), 0f, 0f, Paint().apply {
                xfermode = PorterDuffXfermode(PorterDuff.Mode.SCREEN)
            })
 
            canvas.drawBitmap(sampleBitmap, 0f, 0f, Paint())
        }
 
        private fun getCircleBitmap() = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
            val canvas = Canvas(this)
            val paint = Paint().apply {
                flags = Paint.ANTI_ALIAS_FLAG
                color = Color.parseColor("#E9B639")
            }
            val radius = width*3.5f/10
 
            canvas.drawCircle(radius, radius, radius, paint)
        }
 
        private fun getRectangleBitmap() = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
            val canvas = Canvas(this)
            val paint = Paint().apply {
                flags = Paint.ANTI_ALIAS_FLAG
                color = Color.parseColor("#4899C5")
            }
            val startTop = width*3/10
            val endBottom = width*9/10
 
            canvas.drawRect(
                Rect(startTop, startTop, endBottom, endBottom),
                paint
            )
        }
 
    }
 
}
cs

 

 

 

 

 

참고

 

https://ko.wikipedia.org/wiki/%EC%95%8C%ED%8C%8C_%EC%B1%84%EB%84%90

 

알파 채널 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

 

https://developer.android.com/reference/android/graphics/PorterDuff.Mode

 

PorterDuff.Mode  |  Android 개발자  |  Android Developers

 

developer.android.com

 

 

 

'Common' 카테고리의 다른 글

암호화와 해시  (0) 2020.06.19
Git 기본 명령어  (0) 2019.08.19
Android Studio/IntelliJ 단축키 (Window)  (0) 2019.08.17
시간 표시  (0) 2019.06.16

 

 

 

 

 

RecyclerView.ItemDecoration을 이용한 StickyHeader 구현

외부 라이브러리없이 전체 코드로 구현한다.

 

ItemDecoration

RecyclerView를 더 커스텀하여 사용할 수 있게 해준다.

아이템 간의 구분선을 만들거나 여백을 조정할 수 있으며 해당 포스팅처럼 RecyclerView 자체에 그릴 수도 있다.

 

ItemDecoration의 onDrawOver 메소드를 오버라이딩하여 구현한다.

RecyclerView가 draw할 때마다 콜백으로 호출되며 RecyclerView 위에 그려지게 된다.

 

 

RecyclerView의 Header 아이템과 일반 아이템으로 구분이 되어야 한다.

ViewType은 따로 나누지 않아도 상관없으며 구분할 함수만 구현하면 된다.

 

 

 

 

 

 

 

ItemDecoration 코드와 사용 코드

 

 

HeaderItemDecoration.kt

 

더보기
class HeaderItemDecoration(
    recyclerView: RecyclerView,
    private val isHeader: (itemPosition: Int) -> Boolean,
    onClickHeader: ((itemPosition: Int) -> Unit)? = null
) : RecyclerView.ItemDecoration() {
 
    private var currentHeaderToShow: Pair<Int, RecyclerView.ViewHolder>= null
 
    init {
        recyclerView.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
            override fun onChanged() {
                // clear saved header as it can be outdated now
                currentHeaderToShow = null
            }
        })
 
        recyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            // clear saved layout as it may need layout update
            currentHeaderToShow = null
        }
 
        // handle click on sticky header
        onClickHeader?.let {
            initHeaderClickListener(recyclerView, it)
        }
    }
 
    override fun onDrawOver(c: Canvas, recyclerView: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, recyclerView, state)
 
        val topChildView = getTopChildView(recyclerView) ?: return
        val topChildViewPosition = recyclerView.getChildAdapterPosition(topChildView)
        if (topChildViewPosition == RecyclerView.NO_POSITION) return
 
        val headerView = getHeaderViewToShow(topChildViewPosition, recyclerView) ?: return
 
        val contactedNewHeader = getContactedNewHeader(headerView, recyclerView)
        if (contactedNewHeader != null) {
            drawMovedHeader(c, headerView, contactedNewHeader, recyclerView.paddingTop)
        } else {
            drawHeader(c, headerView, recyclerView.paddingTop)
        }
    }
 
    private fun getTopChildView(recyclerView: RecyclerView)
            = recyclerView.findChildViewUnder(
                recyclerView.paddingStart.toFloat(),
                recyclerView.paddingTop.toFloat()
            )
 
    private fun getHeaderViewToShow(topChildItemPosition: Int, recyclerView: RecyclerView): View? {
        recyclerView.adapter ?: return null
 
        val headerPositionToShow = getHeaderPositionToShow(topChildItemPosition)
        if (headerPositionToShow == RecyclerView.NO_POSITION) return null
 
        return getHeaderView(headerPositionToShow, recyclerView)
    }
 
    private fun getHeaderView(headerPositionToShow: Int, recyclerView: RecyclerView): View? {
        val headerHolderType = recyclerView.adapter!!.getItemViewType(headerPositionToShow)
        if (currentHeaderToShow?.first == headerPositionToShow && currentHeaderToShow?.second?.itemViewType == headerHolderType) {
            return currentHeaderToShow?.second?.itemView
        }
 
        val headerHolder = recyclerView.adapter!!.createViewHolder(recyclerView, headerHolderType)
        recyclerView.adapter!!.onBindViewHolder(headerHolder, headerPositionToShow)
        fixLayoutSize(recyclerView, headerHolder.itemView)
 
        currentHeaderToShow = headerPositionToShow to headerHolder
 
        return headerHolder.itemView
    }
 
    private fun getHeaderPositionToShow(topChildItemPosition: Int): Int {
        var headerPosition = RecyclerView.NO_POSITION
        var currentPosition = topChildItemPosition
        do {
            if (isHeader(currentPosition)) {
                headerPosition = currentPosition
                break
            }
            currentPosition -= 1
        } while (currentPosition >= 0)
        return headerPosition
    }
 
    private fun fixLayoutSize(parent: ViewGroup, view: View) {
        // Specs for parent (RecyclerView)
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
 
        // Specs for children (headers)
        val childWidthSpec = ViewGroup.getChildMeasureSpec(
            widthSpec,
            parent.paddingStart + parent.paddingEnd,
            view.layoutParams.width
        )
        val childHeightSpec = ViewGroup.getChildMeasureSpec(
            heightSpec,
            parent.paddingTop + parent.paddingBottom,
            view.layoutParams.height
        )
 
        view.measure(childWidthSpec, childHeightSpec)
        view.layout(00, view.measuredWidth, view.measuredHeight)
    }
 
    private fun drawHeader(c: Canvas, header: View, paddingTop: Int) {
        c.save()
        c.translate(0f, paddingTop.toFloat())
        header.draw(c)
        c.restore()
    }
 
    private fun getContactedNewHeader(headerView: View, recyclerView: RecyclerView): View? {
        val contactPoint = headerView.bottom + recyclerView.paddingTop
        val contactedChildView = getContactedChildView(recyclerView, contactPoint) ?: return null
        val contactedChildViewPosition = recyclerView.getChildAdapterPosition(contactedChildView)
 
        return if (isHeader(contactedChildViewPosition)) {
            contactedChildView
        } else {
            null
        }
    }
 
    private fun getContactedChildView(recyclerView: RecyclerView, contactPoint: Int): View? {
        var childInContact: View? = null
        for (i in 0 until recyclerView.childCount) {
            val child = recyclerView.getChildAt(i)
            val bounds = Rect()
            recyclerView.getDecoratedBoundsWithMargins(child, bounds)
            if (bounds.bottom > contactPoint) {
                if (bounds.top <= contactPoint) {
                    childInContact = child
                    break
                }
            }
        }
        return childInContact
    }
 
    private fun drawMovedHeader(c: Canvas, contactedTopHeader: View, contactedBottomHeader: View, paddingTop: Int) {
        c.save()
        c.translate(0f, (contactedBottomHeader.top - contactedTopHeader.height).toFloat())
        contactedTopHeader.draw(c)
        c.restore()
    }
 
    private fun initHeaderClickListener(recyclerView: RecyclerView, onClickHeader: (Int) -> Unit) {
        recyclerView.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
            override fun onInterceptTouchEvent(
                recyclerView: RecyclerView,
                motionEvent: MotionEvent
            ): Boolean {
                return if (motionEvent.action == MotionEvent.ACTION_DOWN) {
                    val hasClickedOnHeaderArea = motionEvent.y <= currentHeaderToShow?.second?.itemView?.bottom ?: 0
                    if (hasClickedOnHeaderArea) {
                        currentHeaderToShow?.first?.let { position ->
                            onClickHeader.invoke(position)
                        }
                        true
                    } else {
                        false
                    }
                } else false
            }
        })
    }
}
cs

 

 

사용 코드

 

        rv_main.addItemDecoration(
            HeaderItemDecoration(
                rv_main,
                { it%4 == 0 },
                { Toast.makeText(this, it.toString(), Toast.LENGTH_SHORT).show() }
            )
        )
cs

 

HeaderItemDecoration 클래스에

recyclerView, header 판단 함수, header 클릭시 동작 함수를 입력해 사용하면 된다.

 

 

 

 

onDrawOver

    override fun onDrawOver(c: Canvas, recyclerView: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, recyclerView, state)
 
        val topChildView = getTopChildView(recyclerView) ?: return
        val topChildViewPosition = recyclerView.getChildAdapterPosition(topChildView)
        if (topChildViewPosition == RecyclerView.NO_POSITION) return
 
        val headerView = getHeaderViewToShow(topChildViewPosition, recyclerView) ?: return
 
        val contactedNewHeader = getContactedNewHeader(headerView, recyclerView)
        if (contactedNewHeader != null) {
            drawMovedHeader(c, headerView, contactedNewHeader, recyclerView.paddingTop)
        } else {
            drawHeader(c, headerView, recyclerView.paddingTop)
        }
    }
cs

 

onDrawOver 함수에 모든 핵심 내용이 다 있다.

 

1. recyclerView에서 보이는 가장 최상단 아이템 포지션을 얻는다.

2. 최상단 아이템 index 이하의 가장 가까운 header 아이템의 뷰를 얻는다.

3. (기존의 그리고 있는 header) 아래로 (접촉이 있는 새로운 header)가 있는지 확인한다.

 3-1. 있다면 새로운 header가 간섭한 만큼 위로 올려 그린다.

 3-2. 없다면 그대로 그린다.

 

만약 헤더끼리 접촉했을 때 하단 헤더가 상단 헤더를 밀도록 구현하는게 아닌 아래로 들어가듯이 구현하고 싶다면 contactedNewHeaer를 구할 필요없이 drawHeader만 사용하면 된다.

 

 

 

 

 

 

원본 코드 및 참고한 링크

 

원본코드

원본코드에서 필요하다고 생각하는 코드로만 다시 정리했다.

https://gist.github.com/filipkowicz/1a769001fae407b8813ab4387c42fcbd

 

Item Decorator for sticky headers in Kotlin

Item Decorator for sticky headers in Kotlin. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

참고한 링크

StickyHeader를 구현하는 여러 코드들이 있다.

https://www.it-swarm.dev/ko/android/recyclerview%EC%97%90%EC%84%9C-%EB%81%88%EC%A0%81%ED%95%9C-%ED%97%A4%EB%8D%94%EB%A5%BC-%EB%A7%8C%EB%93%A4%EB%A0%A4%EB%A9%B4-%EC%96%B4%EB%96%BB%EA%B2%8C%ED%95%B4%EC%95%BC%ED%95%A9%EB%8B%88%EA%B9%8C-%EC%99%B8%EB%B6%80-lib%EC%97%86%EC%9D%B4/1055032283/

 

android — RecyclerView에서 끈적한 헤더를 만들려면 어떻게해야합니까? (외부 lib없이)

에야디야, 이것은 스크린에서 벗어나기 시작할 때 한 종류의 홀더 스틱을 원한다면 (우리는 어떤 섹션도 신경 쓰지 않습니다) 당신이 그것을하는 방법입니다. 재활용 아이템의 내부 RecyclerView 로

www.it-swarm.dev

 

 

'Android > UI' 카테고리의 다른 글

BottomSheet  (0) 2020.08.10

 

 

 

 

dependency

 

implementation 'com.google.android.material:material:1.3.0-alpha02'

 

 

 

BottomSheetDialog

 

 

기존의 다이얼로그 사용법과 동일하다.

 

        BottomSheetDialog(this).apply {
            val view = layoutInflater.inflate(R.layout.view_bottom_sheet, null)
            view.btn_view_bottom_sheet.setOnClickListener {
                Toast.makeText(this@MainActivity, "FIN", Toast.LENGTH_SHORT).show()
                dismiss()
            }
            setContentView(view)
        }.show()
cs

 

 

 

 

 

 

BottomSheet With Behavior

 

 

 

activity_main.xml

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <!--  Content  -->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="50dp">
 
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello, World!"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
 
    </androidx.constraintlayout.widget.ConstraintLayout>
 
    <!--  BottomSheet  -->
    <LinearLayout
        android:id="@+id/ll_main_bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="#66A9EC"
        app:behavior_hideable="true"
        app:behavior_peekHeight="50dp"
        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
 
        <TextView
            android:id="@+id/tv_main_bottom_sheet_title"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center"
            android:text="TODO List"
            android:textSize="20dp"
            android:textColor="@android:color/black"
            android:textStyle="bold"/>
 
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_main_bottom_sheet"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:paddingBottom="20dp"
            android:clipToPadding="false"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
 
    </LinearLayout>
 
</androidx.coordinatorlayout.widget.CoordinatorLayout>
cs

 

 

behavior_hideable : 완전하게 숨길 수 있을지 없을지를 정한다.

behavior_peekHeight : 최소 높이를 정한다.

layout_behavior : 행동을 정한다. 물론 BottomSheet을 만들기 위해서이기 때문이니

  com.google.android.material.bottomsheet.BottomSheetBehavior로 값을 넣어준다.

 

 

 

 

 

 

MainActivity.kt

 

class MainActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        initViews()
    }
 
    private fun initViews() {
        rv_main_bottom_sheet.adapter = TodoRecyclerAdapter {
            Toast.makeText(this"$it clicked", Toast.LENGTH_SHORT).show()
            hideBottomSheet()
        }.apply {
            updateDataSet(sample)
        }
 
        tv_main_bottom_sheet_title.setOnClickListener {
            showBottomSheet()
        }
    }
 
    private fun showBottomSheet() {
        BottomSheetBehavior.from(ll_main_bottom_sheet).let {
            if (it.state != BottomSheetBehavior.STATE_EXPANDED) {
                it.state = BottomSheetBehavior.STATE_EXPANDED
            }
        }
    }
 
    private fun hideBottomSheet() {
        BottomSheetBehavior.from(ll_main_bottom_sheet).let {
            if (it.state != BottomSheetBehavior.STATE_COLLAPSED) {
                it.state = BottomSheetBehavior.STATE_COLLAPSED
            }
        }
    }
 
    private val sample = IntRange(020).map { "Job $it" }.toList()
 
}
cs

 

 

기본적으로 드래그 액션을 통해서 BottomSheet을 제어할 수 있지만

코드를 통해서 특정상황에 확장하거나 줄일 수 있다.

 

 

 

'Android > UI' 카테고리의 다른 글

RecyclerView - StickyHeader  (0) 2020.08.17

+ Recent posts