30 Aug 2019
|
Kotlin
코틀린은 확장 함수를 지원합니다.
Java의 경우 기존에 만들어진 클래스에 새로운 메소드를 추가하려면 해당 클래스를 상속하는 새로운 클래스를 만들어야 하는데,
코틀린에서는 확장 함수(Extension Function)를 이용해서 상속 없이 기존 클래스에 새로운 함수를 추가할 수 있습니다.
이 때 확장 함수를 추가할 대상 클래스는 리시버 타입(Receiver Type)이라고 합니다.
예를 들어 다음과 같은 방법으로 확장 함수를 추가할 수 있습니다.
fun main(args: Array<String>) {
println("snowdeer".hello())
}
fun String.hello() : String {
return "$this, hello"
}
확장 함수의 사용 방법은 클래스 내 정의된 메소드와 동일하게 점(.)을 찍고 호출할 수 있지만,
실제로는 클래스 외부에서 추가된 함수이기 때문에 함수 내에서는 private나 protected로 선언된 변수나 메소드에 접근할 수 없습니다.
17 Aug 2019
|
Android
화면의 TouchEvent에서 멀티 터치를 이용한 Scale이나 Gesture를 인식하는 Handler 코드 예제입니다.
package com.snowdeer.utils
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
interface OnScaleGestureEventListener {
fun onSingleTab(x: Float, y: Float)
fun onDoubleTab(x: Float, y: Float)
fun onLongPress(x: Float, y: Float)
fun onScaleChanged(scaleFactor: Float)
fun onOffsetChanged(offsetX: Float, offsetY: Float)
}
class ScaleGestureHandler(ctx: Context) {
private var scaleFactor = 1.0F
private var offsetX = 0.0F
private var offsetY = 0.0F
private var scaledOffsetX = 0.0F
private var scaledOffsetY = 0.0F
private var focusX = 0.0F
private var focusY = 0.0F
private val scaleGestureDetector: ScaleGestureDetector
private val gestureDetector: GestureDetector
var onScaleGestureEventListener: OnScaleGestureEventListener? = null
init {
scaleGestureDetector = ScaleGestureDetector(ctx, ScaleListener())
gestureDetector = GestureDetector(ctx, GestureListener())
}
fun handleTouchEvent(event: MotionEvent): Boolean {
scaleGestureDetector.onTouchEvent(event)
if (gestureDetector.onTouchEvent(event)) {
return true
}
return false
}
fun reset() {
scaleFactor = 1.0F
offsetX = 0F
offsetY = 0F
scaledOffsetX = 0F
scaledOffsetY = 0F
focusX = 0.0F
focusY = 0.0F
}
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = Math.max(0.8f, Math.min(scaleFactor, 3.0f))
focusX = detector.focusX
focusY = detector.focusY
onScaleGestureEventListener?.onScaleChanged(scaleFactor)
offsetX = (scaledOffsetX - focusX) * scaleFactor + focusX
offsetY = (scaledOffsetY - focusY) * scaleFactor + focusY
onScaleGestureEventListener?.onOffsetChanged(offsetX, offsetY)
return true
}
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
onScaleGestureEventListener?.onSingleTab(e.x, e.y)
return true
}
override fun onDoubleTap(e: MotionEvent): Boolean {
onScaleGestureEventListener?.onDoubleTab(e.x, e.y)
return true
}
override fun onLongPress(e: MotionEvent) {
onScaleGestureEventListener?.onLongPress(e.x, e.y)
}
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
scaledOffsetX -= distanceX / scaleFactor
scaledOffsetY -= distanceY / scaleFactor
offsetX = (scaledOffsetX - focusX) * scaleFactor + focusX
offsetY = (scaledOffsetY - focusY) * scaleFactor + focusY
onScaleGestureEventListener?.onOffsetChanged(offsetX, offsetY)
return true
}
}
}
16 Aug 2019
|
Android
기존에 Java 버전의 Runtime Permission을 포스팅 했었지만
이번에는 Kotlin 버전으로 포스팅합니다.
private const val PERMISSION_REQUEST_CODE = 1231
class MainActivity : AppCompatActivity() {
private val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!checkPermissions(permissions)) {
requestPermissions(permissions, PERMISSION_REQUEST_CODE)
}
}
private fun checkPermissions(permissions: Array<String>): Boolean {
for (permission in permissions) {
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
return false
}
}
return true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PERMISSION_REQUEST_CODE -> {
if ((grantResults.isNotEmpty()) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
} else {
Toast.makeText(applicationContext, "Permission is not granted.", Toast.LENGTH_SHORT).show()
}
}
}
}
}
11 Aug 2019
|
Android
gradle에서 변수를 사용하는 방법입니다.
예를 들어 특정 라이브러리의 버전을 변수로 지정할 수 있습니다.
build.gradle (Project)
buildscript {
ext.kotlin_version = '1.3.41'
ext.snowlib_version = 'latest.integration'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
build.gradle (Module)
그리고 아래 예제와 같은 방법으로 위에서 선언한 snowlib_version이라는 변수를 사용할 수 있습니다.
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':common_library')
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:design:28.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation group: 'snowdeer.message.utils', name: 'snowdeer-message-util', version: "$snowlib_version", changing: true
implementation group: 'snowdeer.actiontool.library', name: 'snowdeer-actiontool-library', version: "$snowlib_version", changing: true
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
05 Aug 2019
|
Android
먼저 View를 상속받는 캔버스(Canvas)를 가진 클래스를 하나 구현합니다. 그리고 setPercentage 메소드를 만들어줍니다.
클래스 외부에서 애니메이션을 동작시킬 때 필요한 progress 관련 메소드입니다.
class PathView(context: Context, attrs: AttributeSet) : View(context, attrs) {
companion object {
private const val CORNER_ROUND = 40F
}
private val wayPointList = ArrayList<PointF>()
private val paint = Paint()
private var progress = 0F
private var pathLength = 0F
init {
paint.apply {
color = context.getColor(R.color.path_color)
style = Paint.Style.STROKE
isAntiAlias = true
strokeWidth = 8.0F
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
}
initDummyData()
}
private fun initDummyData() {
wayPointList.add(PointF(190F, 1715F))
wayPointList.add(PointF(270F, 1715F))
wayPointList.add(PointF(270F, 650F))
wayPointList.add(PointF(460F, 650F))
wayPointList.add(PointF(460F, 500F))
}
fun setPath(pointList: ArrayList<Point>) {
wayPointList.clear()
for (p in pointList) {
val x = MapCoordinateConverter.getCanvasXFromWorldX(p.x).toFloat()
val y = MapCoordinateConverter.getCanvasXFromWorldX(p.y).toFloat()
wayPointList.add(PointF(x, y))
}
invalidate()
}
fun setPercentage(percentage: Float) {
if (percentage < 0.0f || percentage > 1.0f) {
throw IllegalArgumentException("setPercentage not between 0.0f and 1.0f")
}
progress = percentage
invalidate()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
drawPath(canvas)
}
private fun drawPath(canvas: Canvas?) {
val p = createPath()
val measure = PathMeasure(p, false)
pathLength = measure.length
val total = pathLength - pathLength * progress
val pathEffect = DashPathEffect(floatArrayOf(pathLength, pathLength), total)
val cornerPathEffect = CornerPathEffect(CORNER_ROUND)
paint.pathEffect = ComposePathEffect(cornerPathEffect, pathEffect)
canvas?.drawPath(p, paint)
}
private fun createPath(): Path {
val p = Path()
if(wayPointList.size > 0) {
p.moveTo(wayPointList[0].x, wayPointList[0].y)
for (pf in wayPointList) {
p.lineTo(pf.x, pf.y)
}
}
return p
}
}
위에서 만든 클래스를 외부에서 사용할 때는 다음과 같이 작성하면 됩니다.
class MapView(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {
private val pathView = PathView(context, attrs)
init {
val layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT)
addView(pathView, layoutParams)
}
fun start() {
playPathAnimation(pathView)
}
fun setPath(pointList: ArrayList<Point>) {
pathView.setPath(pointList)
}
private fun playPathAnimation(target: View) {
val anim = ObjectAnimator.ofFloat(target, "percentage", 0.0f, 1.0f)
anim.duration = 3000
anim.interpolator = LinearInterpolator()
anim.start()
}
}