Design-loving front-end engineer
Ryong
Design-loving front-end engineer
전체 방문자
오늘
어제
    • Framework
    • React
      • Concept
      • Library
      • Hook
      • Component
      • Test
    • NodeJS
    • Android
      • Concept
      • Code
      • Sunflower
      • Etc
    • Flutter
      • Concept
      • Package
    • Web
    • Web
    • CSS
    • Language
    • JavaScript
    • TypeScript
    • Kotlin
    • Dart
    • Algorithm
    • Data Structure
    • Programmers
    • Management
    • Git
    • Editor
    • VSCode
    • Knowledge
    • Voice
Design-loving front-end engineer

Ryong

Android/Code

[Project] [Android] [Kotlin] Log Window

2021. 10. 18. 22:39

요약

◽  모든 클래스에서 로그 관련 기능을 참조하여 사용할 수 있도록 LogUtil 클래스 구현

◽  로그를 앱 내에서 볼 수 있는 로그 윈도우 기능 구현

◽  로그 윈도우 터치 드래그앤 드롭 뷰 이동 기능 구현

◽  로그 윈도우 첫 등장 위치를 설정하기 위한 뷰의 width, height 구하는 코드 구현

◽  로그 윈도우 확대/축소 애니메이션을 사용한 뷰 보이기/숨기기 기능 구현

◽  각 클래스마다 TAG 설정하는 방법 및 선언 위치

 

전체 코드

https://github.com/tmdfyd2020/Android-Sources/tree/main/LogWindow

 

GitHub - tmdfyd2020/Android-Sources: Android Toy Project Codes

Android Toy Project Codes. Contribute to tmdfyd2020/Android-Sources development by creating an account on GitHub.

github.com

 

코드

MainActivity

class MainActivity : AppCompatActivity() {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    private val container by lazy { binding.container }
    private val button by lazy { binding.btnMainLog }
    private val logWindow by lazy { binding.layoutMainLogWindow }

    private var shortAnimationDuration: Int = 0
    private var currentAnimator: Animator? = null
    private var zoomState: Boolean = false
    private var logWindowX: Float = 0f
    private var logWindowY: Float = 0f

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        init()
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun init() {
        // Init log window start x, y points
        with (container) {
            viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener1 {
                override fun onGlobalLayout() {
                    val containerWidth = width
                    with (logWindow) {
                        viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener1 {
                            override fun onGlobalLayout() {
                                logWindowX = ((containerWidth - width) / 2).toFloat()
                                logWindowY = (height / 2).toFloat()
                                viewTreeObserver.removeOnGlobalLayoutListener(this)
                            }
                        })
                    }
                    viewTreeObserver.removeOnGlobalLayoutListener(this)
                }
            })
        }

        // LogWindow touch and drag move listener
        var moveX = 0f
        var moveY = 0f
        logWindow.setOnTouchListener { view: View, event: MotionEvent ->
            when(event.action) {
                MotionEvent.ACTION_DOWN -> {
                    moveX = view.x - event.rawX
                    moveY = view.y - event.rawY
                }

                MotionEvent.ACTION_MOVE -> {
                    view.animate()
                        .x(event.rawX + moveX)
                        .y(event.rawY + moveY)
                        .setDuration(0)
                        .start()
                    logWindowX = view.x
                    logWindowY = view.y
                }
            }
            true
        }

        shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
    }

    /**
     * View Expansion and Shrink animation.
     * Start from button x, y to current window's x, y
     * Up to window's original scale
     * @param button start animation view
     */
    private fun zoomAnimation(button: View) {
        currentAnimator?.cancel()

        val startBoundsInt = Rect()
        val finalBoundsInt = Rect()
        val globalOffset = Point()

        button.getGlobalVisibleRect(startBoundsInt)
        container.getGlobalVisibleRect(finalBoundsInt, globalOffset)
        startBoundsInt.offset(-globalOffset.x, -globalOffset.y)
        finalBoundsInt.offset(-globalOffset.x, -globalOffset.y)

        val startBounds = RectF(startBoundsInt)
        val finalBounds = RectF(finalBoundsInt)
        val startScale = startBounds.height() / finalBounds.height()

        logWindow.pivotX = 0f
        logWindow.pivotY = 0f

        when (zoomState) {
            false -> {
                LogUtil.i(TAG, "Animation Open")
                logWindow.visibility = View.VISIBLE
                currentAnimator = AnimatorSet().apply {
                    play(ObjectAnimator.ofFloat(logWindow, View.X, button.x, logWindowX)).apply {  // X 시작 위치, 마지막 위치
                        with(ObjectAnimator.ofFloat(logWindow, View.Y, button.y, logWindowY))  // Y 시작 위치, 마지막 위치
                        with(ObjectAnimator.ofFloat(logWindow, View.SCALE_X, startScale, 1f))  // X 크기
                        with(ObjectAnimator.ofFloat(logWindow, View.SCALE_Y, startScale, 1f))  // Y 크기
                    }
                    duration = shortAnimationDuration.toLong()
                    interpolator = DecelerateInterpolator()
                    addListener(object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator) {
                            currentAnimator = null
                        }

                        override fun onAnimationCancel(animation: Animator) {
                            currentAnimator = null
                        }
                    })
                    start()
                }
                zoomState = true
            }
            true -> {
                LogUtil.i(TAG, "Animation Close")
                currentAnimator = AnimatorSet().apply {
                    play(ObjectAnimator.ofFloat(logWindow, View.X, button.x)).apply {
                        with(ObjectAnimator.ofFloat(logWindow, View.Y, button.y))
                        with(ObjectAnimator.ofFloat(logWindow, View.SCALE_X, startScale))
                        with(ObjectAnimator.ofFloat(logWindow, View.SCALE_Y, startScale))
                    }
                    duration = shortAnimationDuration.toLong()
                    interpolator = DecelerateInterpolator()
                    addListener(object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator) {
                            logWindow.visibility = View.GONE
                            currentAnimator = null
                        }

                        override fun onAnimationCancel(animation: Animator) {
                            logWindow.visibility = View.GONE
                            currentAnimator = null
                        }
                    })
                    start()
                }
                zoomState = false
            }
        }
    }

    fun onClick(view: View) {
        when(view.id) {
            R.id.btn_main_log -> {
                zoomAnimation(view)
            }

            R.id.iv_main_minimize_popup -> {
                with (binding) {
                    layoutMainLogWindowBody.visibility = View.GONE
                    ivMainMinimizePopup.visibility = View.GONE
                    ivMainMaximizePopup.visibility = View.VISIBLE
                }
            }

            R.id.iv_main_maximize_popup -> {
                with (binding) {
                    layoutMainLogWindowBody.visibility = View.VISIBLE
                    ivMainMinimizePopup.visibility = View.VISIBLE
                    ivMainMaximizePopup.visibility = View.GONE
                }
            }

            R.id.iv_main_close_popup -> {
                zoomAnimation(button)
            }
        }
    }

    /**
     * Float log messages at log window
     * @param message log message
     */
    fun writeLog(message: String) {
        val totalMsg = "$message\n${binding.tvMainLogWindow.text}"
        with (binding.tvMainLogWindow) {
            text = totalMsg
        }
    }

    init {
        instance = this
    }

    companion object {
        private const val TAG = "MainActivity"
        private lateinit var instance: MainActivity
        fun instance(): MainActivity { return instance }
    }
}

 

LogUtil

class LogUtil {

    companion object {
        private const val TAG = "LogWindow"

        fun v(tag: String, msg: String) {
            val message = buildLogMsg(tag, msg)
            Log.v(TAG, message)
            CoroutineScope(Dispatchers.Main).launch {
                instance().writeLog(message)
            }
        }

        fun i(tag: String, msg: String) {
            val message = buildLogMsg(tag, msg)
            Log.i(TAG, message)
            CoroutineScope(Dispatchers.Main).launch {
                instance().writeLog(message)
            }
        }

        fun d(tag: String, msg: String) {
            val message = buildLogMsg(tag, msg)
            Log.d(TAG, message)
            CoroutineScope(Dispatchers.Main).launch {
                instance().writeLog(message)
            }
        }

        fun w(tag: String, msg: String) {
            val message = buildLogMsg(tag, msg)
            Log.w(TAG, message)
            CoroutineScope(Dispatchers.Main).launch {
                instance().writeLog(message)
            }
        }

        fun e(tag: String, msg: String) {
            val message = buildLogMsg(tag, msg)
            Log.e(TAG, message)
            CoroutineScope(Dispatchers.Main).launch {
                instance().writeLog(message)
            }
        }

        private val mStringBuffer = StringBuffer()
        private fun buildLogMsg(tag: String, message: String): String {
            val stackTrace = Thread.currentThread().stackTrace[4]
            mStringBuffer.setLength(0)
            mStringBuffer.append("[ $tag ] ")
            mStringBuffer.append("[ ${stackTrace.methodName}() ] ")
            mStringBuffer.append("[ line ${stackTrace.lineNumber} ] ")
            mStringBuffer.append(": $message")
            return mStringBuffer.toString()
        }
    }
}

 

activity_main.xml

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <!-- 로그 윈도우 -->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/layout_main_log_window"
        android:layout_width="350dp"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginTop="30dp"
        android:background="@drawable/bg_layout_round_aablack"
        android:visibility="invisible">
 
        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/layout_main_log_window_header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginBottom="5dp">
 
            <ImageView
                android:id="@+id/iv_main_minimize_popup"
                android:layout_width="30dp"
                android:layout_height="30dp"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/iv_main_close_popup"
                app:layout_constraintBottom_toBottomOf="parent"
                android:layout_margin="5dp"
                android:src="@drawable/ic_minimize_popup"
                android:scaleType="fitXY"
                android:onClick="onClick" />
 
            <ImageView
                android:id="@+id/iv_main_maximize_popup"
                android:layout_width="30dp"
                android:layout_height="30dp"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/iv_main_close_popup"
                app:layout_constraintBottom_toBottomOf="parent"
                android:layout_margin="5dp"
                android:src="@drawable/ic_maximize_popup"
                android:scaleType="fitXY"
                android:visibility="gone"
                android:onClick="onClick" />
 
            <ImageView
                android:id="@+id/iv_main_close_popup"
                android:layout_width="30dp"
                android:layout_height="30dp"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                android:layout_margin="5dp"
                android:src="@drawable/ic_close_popup"
                android:scaleType="fitXY"
                android:onClick="onClick" />
 
        </androidx.constraintlayout.widget.ConstraintLayout>
 
        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/layout_main_log_window_body"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            app:layout_constraintTop_toBottomOf="@+id/layout_main_log_window_header"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent">
 
            <ScrollView
                android:id="@+id/scroll_main_log_window"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                android:layout_marginBottom="20dp"
                android:scrollbarSize="7dp"
                android:scrollbarThumbVertical="@drawable/scrollbar_thumb"
                android:scrollbarTrackVertical="@drawable/bg_layout_round_white_15dp">
 
                <TextView
                    android:id="@+id/tv_main_log_window"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginHorizontal="10dp"
                    android:textSize="16sp"
                    android:textColor="@color/white" />
 
            </ScrollView>
 
        </androidx.constraintlayout.widget.ConstraintLayout>
 
    </androidx.constraintlayout.widget.ConstraintLayout>
 
    <ImageView
        android:id="@+id/btn_main_log"
        android:layout_width="40dp"
        android:layout_height="40dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:src="@drawable/ic_main_window"
        android:onClick="onClick" />
 
</androidx.constraintlayout.widget.ConstraintLayout>
 
cs

 

저작자표시 (새창열림)

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

[ Project ] [ Android ] [ Kotlin ] 콜백 함수  (0) 2021.11.16
[Android] [Kotlin] 객체 인스턴스  (0) 2021.11.05
[Android] [Kotlin] Google Map  (0) 2021.09.24
[Android] [Kotlin] EditText 자동 포커싱 및 키보드  (0) 2021.09.23
[Android] [Kotlin] Dialog로 액티비티 띄우기  (0) 2021.09.15
    'Android/Code' 카테고리의 다른 글
    • [ Project ] [ Android ] [ Kotlin ] 콜백 함수
    • [Android] [Kotlin] 객체 인스턴스
    • [Android] [Kotlin] Google Map
    • [Android] [Kotlin] EditText 자동 포커싱 및 키보드
    Design-loving front-end engineer
    Design-loving front-end engineer
    디자인에 관심이 많은 모바일 앱 엔지니어 Ryong입니다.

    티스토리툴바