시작하기.

   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        enableEdgeToEdge() // 핸드폰의 앳지사용이 표시된다.
      
    }

 

 

수정자와 뷰 기본

/* compose 수정자 
 * 컴포저블의 크기,레이아웃,동작및 모양 크기를 변경한다.
 * 접근성 라벨과 같은 정보를 추가한다.
 * 사용자 입력 처리
 * 요소 클릭 가능,스크롤 가능, 드래그 가능 또는 확대축소 가능하게 만드는 상호작용을 추가한다.
 */


// modifier 예제

//뷰를 그리는 방법 

@composable 
fun exampleView(name:String ){

   // Column ->  세로배치
   // Row -> 가로배치
   // Box -> 다른요소 위에배치
   
	Column(
    //생성자추가 가능
   modifiter = Modifiter 
   .padding(10.dp) //이런식으로 크기조절 가능
   .verticalArrangement = Arrangement.spacedBy(8.dp) //정렬 
   //참고사이트 정렬 "https://nosorae.tistory.com/entry/AndroidCompose-%ED%97%B7%EA%B0%88%EB%A0%A4%EC%84%9C-%EB%94%B1-%EC%A0%95%EB%A6%AC%ED%95%98%EB%8A%94-Compose-%EC%A0%95%EB%A0%ACAlignment%EA%B3%BC-%EB%B0%B0%EC%B9%98Arrangement"
    
    ){
    //작성가능한 뷰 아이템 들
    //Button , spacer , Text ,  Card , Dialog , LazyRow 등등 
      Text( text = "This is a full screen dialog",textAlign = TextAlign.Center )
      TextButton(onClick = { onDismissRequest() }) {  Text("Dismiss") }
    


}

 

 

리사이클러뷰

  //jetpack compose 의 리사이클러뷰 
  @Composable
    fun CustomRecyclerView(profiles: List<profile>) {

        LazyRow(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            content = {
                itemsIndexed(profiles) { index, user ->
                    CustomUserProfile(
                        id = user.id,
                        name = user.name,
                        age = user.age,
                        nickName = user.nickName,
                        hobby = user.hobby
                    )

                }
            })


    }

    @Composable
    fun CustomUserProfile(id: Int, name: String, age: Int, nickName: String, hobby: String) {

        Card(
            modifier = Modifier
                .padding(20.dp, 150.dp)
                .wrapContentWidth()
                .height(130.dp)
                .wrapContentWidth(Alignment.Start),
            shape = RoundedCornerShape(15.dp),
            elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
            colors = CardDefaults.cardColors(
                containerColor = Color(0xFFECEFF1),
            )
        ) {
            Row(
                modifier = Modifier
                    .fillMaxHeight()
                    .padding(top = 12.dp, bottom = 12.dp, start = 12.dp, end = 30.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                // 프로필 이미지나 아이콘을 여기에 넣을 수 있습니다.
                /*          Icon(
                              imageVector = Icons.Filled.AccountCircle,
                              contentDescription = "Profile",
                              modifier = Modifier
                                  .size(60.dp)
                                  .padding(4.dp),
                              tint = Color.Gray
                          )*/

                Spacer(modifier = Modifier.width(12.dp))

                Column {
                    Text(
                        text = "# $id",
                        style = TextStyle(
                            color = Color.Black,
                            fontSize = 22.sp,
                            fontWeight = FontWeight.Bold
                        )
                    )

                    Spacer(modifier = Modifier.height(4.dp))

                    Text(
                        text = "$name a.k.a $nickName",
                        style = TextStyle(
                            color = Color.Black,
                            fontSize = 18.sp,
                            fontWeight = FontWeight.Bold
                        )
                    )

                    Spacer(modifier = Modifier.height(4.dp))

                    Text(
                        text = "Age: $age",
                        style = TextStyle(
                            color = Color.DarkGray,
                            fontSize = 14.sp
                        )
                    )

                    Spacer(modifier = Modifier.height(4.dp))

                    Text(
                        text = "Hobby: $hobby",
                        style = TextStyle(
                            color = Color.DarkGray,
                            fontSize = 14.sp
                        )
                    )
                }
            }
        }

    }

 

 

다이얼로그 띄우기

 @Composable
    fun DialogExamples() {
        var currentProgress = remember { mutableStateOf(0f) }
        val openAlertDialog = remember { mutableStateOf(false) }
        val fullScreenDialog = remember { mutableStateOf(false) }
        var loading = remember { mutableStateOf(false) }
        val scope = rememberCoroutineScope() // Create a coroutine scope

        Column(
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth(),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Spacer(
                modifier = Modifier
                    .padding(20.dp)
                    .fillMaxWidth()
                    .height(200.dp)
            )
            Button(
                onClick = {
                    loading.value = true
                    scope.launch {
                        loadProgress {progress ->
                            currentProgress.value = progress
                        }
                        fullScreenDialog.value = !fullScreenDialog.value
                    }
                    loading.value = false
                }, enabled = !loading.value) {
                Text("Alert dialog component with buttons")
            }
            if(loading.value){
                LinearProgressIndicator(
                    progress = currentProgress.value,
                    modifier =  Modifier.fillMaxWidth()
                )
            }


            Button(
                onClick = { openAlertDialog.value = !openAlertDialog.value }
            ) {
                Text("Alert dialog component with buttons")
            }
        }
        when {
            openAlertDialog.value -> {
                MinimalDialog(
                    onDismissRequest = { openAlertDialog.value = false },

                    )
            }

            fullScreenDialog.value -> {
                FullScreenDialog(
                    onDismissRequest = { fullScreenDialog.value = false },

                    )
            }
        }
    }
    
    
      @Composable
    fun MinimalDialog(onDismissRequest: () -> Unit) {
        Dialog(onDismissRequest = { onDismissRequest() }) {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(200.dp)
                    .padding(16.dp),
                shape = RoundedCornerShape(16.dp),
            ) {
                Text(
                    text = "This is a minimal dialog",
                    modifier = Modifier
                        .fillMaxSize()
                        .wrapContentSize(Alignment.Center),
                    textAlign = TextAlign.Center,
                )

            }
        }
    }

    @Composable
    fun FullScreenDialog(onDismissRequest: () -> Unit) {
        Dialog(
            onDismissRequest = { onDismissRequest() },
            properties = DialogProperties(
                usePlatformDefaultWidth = false,
                dismissOnBackPress = true,
            ),
        ) {
            Surface(
                modifier = Modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.surfaceVariant),
            ) {
                Column(
                    modifier = Modifier
                        .fillMaxSize(),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally,
                ) {
                    Text(
                        text = "This is a full screen dialog",
                        textAlign = TextAlign.Center,
                    )
                    TextButton(onClick = { onDismissRequest() }) {
                        Text("Dismiss")
                    }
                }
            }
        }
    }

백그라운드 상태의 앱이 포그라운드 서비스 이용중일때 사용자에게 알림과 동시에 앱의 종료를 막기위한 보조장치로 사용하였다.

 

SYSTEM__ALERT_WINDOW 기능을 이용한 라이브러리를 사용.

 

또한 포그라운드와 백그라운드 상태를 받기위하여 라이프사이클 프로세스 사용.

 

gradle

//floatingview
implementation 'com.github.recruit-lifestyle:FloatingView:2.4.4'

//라이프사이클 프로세스
implementation "androidx.lifecycle:lifecycle-runtime:2.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"

 

manifest

 sdk 33이상은 post_notifications 권한을 받아야한다.

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"  android:minSdkVersion="33" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

 

앱 플러팅을 띄울 이미지뷰 

<?xml version="1.0" encoding="utf-8"?>

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

 <ImageView
     android:layout_width="96dp"
     android:layout_height="96dp"
     android:background="@drawable/bg_main_box"
     android:scaleType="center"
     android:src="@mipmap/app_icon"></ImageView>
</LinearLayout>

 

service

class FloatingViewService : Service(), FloatingViewListener {
    var mFloatingViewManager: FloatingViewManager? = null
    var builder: NotificationCompat.Builder =   NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_LOCATION)
    var notiText = ""
    val EXTRA_CUTOUT_SAFE_AREA ="test"
        override fun onBind(intent: Intent?): IBinder? {
        TODO("Not yet implemented")
    }

    //사용자가 x 버튼에 넣었을때 콜백
    override fun onFinishFloatingView() {
        stopSelf()
    }

    //사용자가 아이콘을 들었다놨따 할때 위치
    override fun onTouchFinished(isFinishing: Boolean, x: Int, y: Int) {
    }

    override fun onDestroy() {
        stopForeground(2)
        super.onDestroy()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("보리 action", intent?.action.toString())
        if(intent?.action=="stopFloating"){
            stopForeground(2)
            stopSelf()
        }else{
            if (mFloatingViewManager != null) {
                return START_STICKY
            }

            val inflater = LayoutInflater.from(this)
            val iconView = inflater.inflate(R.layout.floating_view, null, false)

            mFloatingViewManager = FloatingViewManager(this, this)
            mFloatingViewManager!!.setFixedTrashIconImage(R.drawable.ico_trash)
            mFloatingViewManager!!.setActionTrashIconImage(R.drawable.ico_trash)
            mFloatingViewManager!!.setSafeInsetRect(intent!!.getParcelableExtra(EXTRA_CUTOUT_SAFE_AREA));
            mFloatingViewManager!!.setDisplayMode(FloatingViewManager.DISPLAY_MODE_SHOW_ALWAYS);

            val options = loadOptions()
            mFloatingViewManager!!.addViewToWindow(iconView, options)

            notiText = "floating"
            startForeground(2, getNotification(notiText))

            iconView.setOnClickListener {
                Log.d("보리 action " , intent?.action.toString())

                Toast.makeText(this@FloatingViewService, "앱으로 이동합니다.", Toast.LENGTH_SHORT).show()
                var mainIntent = Intent(this, MainActivity::class.java).let { mainIntent ->
                    mainIntent.putExtra(INTENT_KEY_CLICK_NOTI, true)
                    mainIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    mainIntent.setAction(ACTION_STOP_FLOATING)
                }
                // Kotlin
                val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    PendingIntent.getActivity(this, 0, mainIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
                } else {
                    PendingIntent.getActivity(this, 0, mainIntent, 0)
                }
                intent.send()
                if (iconView.parent != null) mFloatingViewManager!!.removeAllViewToWindow()


                stopSelf()
                stopForeground(2)
            }
        }



        return  START_REDELIVER_INTENT
    }

    private fun loadOptions(): FloatingViewManager.Options?{
        val options = FloatingViewManager.Options()
        options.shape = FloatingViewManager.SHAPE_CIRCLE
        options.moveDirection = FloatingViewManager.MOVE_DIRECTION_NONE
        options.floatingViewHeight = 289
        val defaultX = (Math.random() * 1000).toInt()
        val defaultY = (Math.random() * 3000).toInt()
        options.floatingViewX = defaultX
        options.floatingViewY = defaultY
        return options
    }

    private fun getNotification(text: String): Notification? {
        val intent = Intent(this, MainActivity::class.java)
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
        val pendingIntent =
            PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
        builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_LOCATION)
            .setSmallIcon(R.drawable.ico_car_bedge)
            .setContentTitle(getString(R.string.app_name))
            .setContentText(text)
            .setContentIntent(pendingIntent)
            .setAutoCancel(false)
        return builder.build()
    }

}

-헬창코딩님의 블로그 참조.

 

백그라운드의 상태를 체크하기위한 리스너

class CycleListne(activity:Activity) : LifecycleObserver {
    val context = activity
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun onMoveToFoground() {
      //  stopFloatingViewService(this.context,true)
        Log.d("보리 lifecyclerObserver","foground"+context.localClassName)

    }
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun onMoveToBackground() {
        startFloatingViewService(this.context,true)
        Log.d("보리 lifecyclerObserver","background"+context.localClassName)
    }


    //권한이 승인되었다면 호출됨
    fun startFloatingViewService(activity: Activity, isCustomFloatingView: Boolean) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            if (activity.window.attributes.layoutInDisplayCutoutMode == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER) {
                throw RuntimeException("'windowLayoutInDisplayCutoutMode' do not be set to 'never'")
            }
        }
        // launch service
        val service: Class<out Service?>
        val key: String
        if (isCustomFloatingView) {
            service = FloatingViewService::class.java
            key = "12"
        } else {
            service = FloatingViewService::class.java
            key = "12"
        }
        val intent = Intent(activity, service)
        intent.putExtra(key, FloatingViewManager.findCutoutSafeArea(activity))
        intent.action = null
        ContextCompat.startForegroundService(activity, intent)
    }

}

 

플러팅뷰를 사용할 공통 activity 

showFloatingView(this,true,true)

 

플러팅뷰 권한체크 및 동작

//floatingView
 fun showFloatingView(context: Context, isShowOverlayPermission: Boolean, isCustomFloatingView: Boolean) {
    // API22
    val CHATHEAD_OVERLAY_PERMISSION_REQUEST_CODE = 100
    val CUSTOM_OVERLAY_PERMISSION_REQUEST_CODE = 101

    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
        return
    }

    // 다른앱위에 표시 할수 있는지 체크
    if (Settings.canDrawOverlays(context)) {
        return
    }

    // 오버레이 퍼미션 체크
    if (isShowOverlayPermission) {
        val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.packageName))
        startActivityForResult(intent, if (isCustomFloatingView) CUSTOM_OVERLAY_PERMISSION_REQUEST_CODE else CHATHEAD_OVERLAY_PERMISSION_REQUEST_CODE)
    }
}



//권한이 승인되었다면 호출됨
fun startFloatingViewService(activity: Activity, isCustomFloatingView: Boolean) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        if (activity.window.attributes.layoutInDisplayCutoutMode == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER) {
            throw RuntimeException("'windowLayoutInDisplayCutoutMode' do not be set to 'never'")
        }
    }
    // launch service
    val service: Class<out Service?>
    val key: String
    if (isCustomFloatingView) {
        service = FloatingViewService::class.java
        key = "12"
    } else {
        service = FloatingViewService::class.java
        key = "12"
    }
    val intent = Intent(activity, service)
    intent.putExtra(key, FloatingViewManager.findCutoutSafeArea(activity))
    intent.action = null
    ContextCompat.startForegroundService(activity, intent)
}

 

라이프사이클 체크 옵저빙

ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleListener)

 

동작 -> 안드로이드 3가지 버튼 백그라운드로이동관련 을 이용시 라이프 사이클에서 백그라운드 및 포그라운드를 체크 -> 백그라운드시 floatingView 를 띄움 -> 플러팅뷰를 클릭시 main으로 이동 및 삭제 

 

//SDK 33 이상 알람설정 권한 체크
fun alarmPermission(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
        ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), ReqBackGroundCode)
}
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"  android:minSdkVersion="33" />

 

안드로이드에서 서버와 통신중 응답이 10초이상 지연되는 api가 있었는데

해당 api는 항상 Timeout Exception 에 빠졌다.

그 이유는 다음과같다.

okHttpClient 설정이 기본 10초로 맞춰져 있기에 네트워크 통신하는 과정의 설정을 바꿔주어야한다.

 

        OkHttpClient.Builder()
            .connectTimeout(1, TimeUnit.MINUTES)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)

 

전부 확인하진 못했지만 .connectTimeout만 설정했을때는 이전과 마찬가지로 10초후 Exception이 떨어져 다른곳에서 문제가 발생하는줄 알았지만

밑에 readTimeout 과 writeTimeout을 같이 사용하니 해당 문제에서 벗어날 수 있었다.

 

끝-

 

 
override fun onBackPressed() {

    //super.onBackPressed()
}

super.onBackPressd() 주석처리

사진권한이 33이상부터는 세분화 되었다고 한다. 
기존 스토리지 접근이 33버전부터는 인식을 못하니 READ_MEDIA_IMAGES로 버전별 적용을 진행하도록하자.

이전버전은 그대로 스토리지하시면 됩니다. 

ANDROID 13 이상 
ANDROID 13 미만 

이렇게 2개로 구분하시고

테스트는 핸드폰 소프트웨어정보 가시면 안드로이드 버전 쉽게 확인 가능.

  //권한 체크
    fun checkPermission():Boolean{
        if(Build.VERSION.SDK_INT >= 33){
            when{
                ContextCompat.checkSelfPermission(getContext(),Manifest.permission.CAMERA )  == PackageManager.PERMISSION_GRANTED
                        &&
                 ContextCompat.checkSelfPermission(getContext(),Manifest.permission.READ_MEDIA_IMAGES )  == PackageManager.PERMISSION_GRANTED
                -> {
                    return true
                }
                else -> {
                    requestPermissions(arrayOf(Manifest.permission.CAMERA,Manifest.permission.READ_MEDIA_IMAGES), PERMISSION_CODE)
                    return false
                }
            }
        }
        else if (Build.VERSION.SDK_INT < 33 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            when {
                ContextCompat.checkSelfPermission(getContext(),
                    Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&
                        ContextCompat.checkSelfPermission(getContext(),
                            Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
                        ContextCompat.checkSelfPermission(getContext(),
                            Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
                -> {
                    //권한 있음
                    return true
                }
//                //사용자에게 권한에 대한 설명, 거부한 경우
//                shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)->{
//                }
                else -> {
                    requestPermissions(arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_CODE)
                    return false
                }
            }
        }
        return false
    }

 

 

 

 

안드로이드 빌드에러 인데 ..

걍 컴터 껏다키면 됨 해결하려 시도하면 손해 빌드를 여러회 바꿔가며 실행했을때 안드로이드스튜디오가 토하는 느낌 

 

컴퓨터 껏다키니까 해결

drag and drop 을 만들었는데 잘못쓰겠다는 아저씨들이 있어서 만들게 되었다.

 

올드한 아재들을 위한 버튼형식의 이동이다.

 

1. 아이템 최상단으로 이동

2. 아이템 상단이동 ( 트레이드 )

3. 아이템 하단이동 ( 트레이드 )

4. 아이템 최하단으로 이동

 

간단한 콜백 리스너 등록과 꾸준한 삽질로 제작하였다.

 

 

1. adpater 의 클릭이벤트 등록

 

Adapter.kt

binding.taskChangeUpIv.setOnClickListener {
    if(position > 1)
       callBack.onUpClick(position)
}
binding.taskChangeDownIv.setOnClickListener {
    if(position < maxSize - 2)
        callBack.onDownClick(position)
}
binding.taskChangeFullUpIv.setOnClickListener {
    if(position > 0)
       callBack.onFullUpClick(position,1)
}
binding.taskChangeFullDownIv.setOnClickListener {
    if(position < maxSize - 2)
        callBack.onFullDownClick(position,maxSize-2)
}

우리 똑똑한 개발자님들은 이름만봐도 대충 뭔지 아실거라 믿는다. 

 

callback.kt

콜백에 리스너 등록

fun onUpClick(position: Int)
fun onDownClick(position: Int)
fun onFullUpClick(position: Int,minPosition: Int)
fun onFullDownClick(position: Int,maxPosition: Int)

activity.kt

override fun onUpClick(position: Int) {
    viewModel.changeList.value?.let {
        val selectData = it[position]
        val upData = it[position - 1]
        it[position] = upData
        it[position - 1] = selectData
        setRecyclerView(it, true, isChange = true)
    }
}

override fun onDownClick(position: Int) {
    viewModel.changeList.value?.let {
        val selectData = it[position]
        val upData = it[position + 1]
        it[position] = upData
        it[position + 1] = selectData
        setRecyclerView(it, true, isChange = true)
    }
}
override fun onFullUpClick(position: Int,minPosition :Int) {
    viewModel.changeList.value?.let {
        val selectData = it[position]
        val clone = it.clone() as ArrayList<WorkDetailDto>
        it[minPosition] = selectData
        for(i in minPosition until position){
            it[i+1] = clone[i]
        }


        setRecyclerView(it, true, isChange = true)
    }
}

override fun onFullDownClick(position: Int,maxPosition : Int) {
    viewModel.changeList.value?.let {
        val selectData = it[position]
        val clone = it.clone() as ArrayList<WorkDetailDto>
        it[maxPosition] = selectData
        for(i in position until maxPosition){
            it[i] = clone[i+1]
        }
        setRecyclerView(it, true, isChange = true)
    }
}

간단하게 람다를 이용하면 깔끔하게 사용할 수 있다.

1. it -> 아이템 정보를 가져오는 arraylist 의 클론을 가져온다. 

2. 클론에 클론을 따서 저장해두고

3. 변경할 포지션은 미리 선점해서 바꾼다.

4. 나머지 변경될 부분은 간단한 람다포문을 이용하여 클론의 데이터와 변경해준다.

5. 리사이클러뷰에 set 해준다.

 

분명 버튼으로 바꿔버리면 드래그드롭 왜 안되냐고 하는 사용자가 있을거 같아서 드래그앤 드롭도 유지하고 버튼도 만들어서 명분을 제거하였다 .

-끝- 

+ Recent posts