요약
◽ 모든 클래스에서 로그 관련 기능을 참조하여 사용할 수 있도록 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 |