nneaning / meaning_Android

๐ŸŽดํ•œ ๋—์— 5์–ต์„ ์•ˆ๋“œ๋กœ๋ฏธ๋‹? ์ซ„๋ฆฌ๋ฉด ์ฃผ๋ฌด์‹œ๋˜์ง€...๐Ÿ’ซ

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool


๐ŸŒœ ๋ฏธ๋ผํด ๋ชจ๋‹์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋‹น์‹ ์˜ ์˜๋ฏธ์žˆ๋Š” ์•„์นจ, meaning ๐ŸŒ


๐Ÿ‘‡ meaning_Android ๐Ÿ‘‡



๐ŸŽด About US

์ง„์ˆ˜ ํšจ์†ก ํ˜•์ค€

Contact:parkjinsu4755@gmail.com

GitHub:jinsu4755

Contact:gythd1998@gmail.com

GitHub:hyooosong

Contact: leehj1232@naver.com

GitHub:LEE-HYUNGJUN


๐Ÿฆ‚์ง„์ˆ˜

- ํƒ€์ž„์Šคํ…œํ”„ ์นด๋ฉ”๋ผ
- ๋กœ๊ทธ์ธ
- ์˜จ๋ณด๋”ฉ
- ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๋กœ์ง ๊ตฌํ˜„

๐ŸŽ… ํšจ์†ก

- ๊ทธ๋ฃน ํƒญ
- ์บ˜๋ฆฐ๋” ๋ทฐ
- sharedPreferences ์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด ๊ตฌํ˜„

๐Ÿ‘จ ๐Ÿ‘ง ํ˜•์ค€

- ํ™ˆ ๋ฉ”์ธํŽ˜์ด์ง€
- ์นด๋“œ ๋ทฐ ์• ๋‹ˆ๋ฉ”์ด์…˜
- ๋งˆ์ด ํ”ผ๋“œ, ๊ทธ๋ฃน ํ”ผ๋“œ
- ํ”ผ๋“œ ์ƒ์„ธ๋ณด๊ธฐ ๋ทฐ

๐Ÿ† Meeting Log


๐Ÿ“ List

1. [Service]

2. [Andromeaning Development Environment]

3. [Work Flow]

4. [Dependencies]

5. [Team Role]

  • [Andromeaning Conventions]
  • [Andromeaning Coding Style]
  • [Code Review Guideline]
  • [Git]

6. [meaning Tech Stack]

7. [Packaging]

8. [Main Feature Codes & Methods]


๐Ÿ’ซ Service about meaning

๋ชจ๋“  ๊ฒƒ์€ ๋ฐ”๋€” ์ˆ˜ ์žˆ๊ณ  ๋‚˜ ์—ญ์‹œ ๋ฌด์–ธ๊ฐ€๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์ƒ์‹œ๊ฐ„์ด ๋‹ฌ๋ผ์ง„๋‹ค๋ฉด, ๋‹น์‹ ๋„ ๋ณ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โ€˜๋‚ดโ€™๊ฐ€ ๋ˆˆ ๋œจ๋Š” ์‹œ๊ฐ„์ด ์•„๋‹Œ, โ€˜ํ•ดโ€™๊ฐ€ ๋œจ๋Š” ์‹œ๊ฐ„๋ถ€ํ„ฐ ํ•˜๋ฃจ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๋ฏธ๋ผํด ๋ชจ๋‹.

๋ฏธ๋‹์„ ํ†ตํ•ด ๋ฏธ๋ผํด ๋ชจ๋‹์— ๋„์ „ํ•˜๋ฉฐ ๋‹น์‹ ๋งŒ์˜ ์˜๋ฏธ์žˆ๋Š” ์•„์นจ์„ ๋งŒ๋“ค์–ด ๋‚˜๊ฐ€๋ณด์„ธ์š”.

์ผ์ฐ ์ผ์–ด๋‚˜๋Š” ์Šต๊ด€์œผ๋กœ ํ•˜๋ฃจ๋ฅผ ๊ธธ๊ฒŒ ๋ณด๋‚ด๋ฉด, ์„ฑ์žฅ์˜ ๋ฐœํŒ์„ ๋งˆ๋ จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฏธ๋‹๊ณผ ํ•จ๊ป˜ ์ฒด๊ณ„์ ์ธ ๊ณ„ํš์„ ์„ธ์šฐ๊ณ  ์ด๋ฅผ ๊ทœ์น™์ ์œผ๋กœ ์‹ค์ฒœํ•˜๋ฉด์„œ ์„ฑ์ทจ๊ฐ์„ ์–ป์–ด๋ณด์„ธ์š”.

์„ฑ์žฅ์ง€ํ–ฅ์ ์ธ ๊ทธ๋ฃน์›๊ณผ ๋ชฉํ‘œ๋ฅผ ๊ณต์œ ํ•œ๋‹ค๋ฉด ์šฐ๋ฆฌ๋Š” ํ•จ๊ป˜, ๋” ๋ฉ€๋ฆฌ ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.



๐Ÿ’ซ Development Environment

Android_Studio Kotlin


๐Ÿ’ซ Work Flow

๐Ÿ’ซ Dependencies

Name Gradle
kotlin org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version
Android KTX implementation 'androidx.core:core-ktx:1.3.2
Design androidx.appcompat:appcompat:1.2.0
com.google.android.material:material:1.2.1
androidx.constraintlayout:constraintlayout:2.0.4
androidx.legacy:legacy-support-v4:1.0.0
viewModel init support androidx.activity:activity-ktx:1.1.0
androidx.fragment:fragment-ktx:1.2.5
LiveData and ViewModel (Arch components) androidx.lifecycle:lifecycle-livedata-ktx:2.2.0
androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0
retrofit com.squareup.retrofit2:retrofit:2.9.0
com.squareup.retrofit2:converter-gson:2.9.0
com.squareup.okhttp3:logging-interceptor:4.9.0
Gson com.google.code.gson:gson:2.8.6
CameraX core library using camera2 implementation androidx.camera:camera-core:$camerax_version
androidx.camera:camera-camera2:$camerax_version
CameraX Lifecycle Library androidx.camera:camera-lifecycle:$camerax_version
CameraX View class androidx.camera:camera-view:1.0.0-alpha20
Test junit:junit:4.13.1
androidx.test.ext:junit:1.1.2
androidx.test.espresso:espresso-core:3.3.0
image load com.github.bumptech.glide:glide:4.11.0
com.github.bumptech.glide:compiler:4.11.0
splash lottie com.airbnb.android:lottie:3.5.0

  • Material Design Component ๊ตฌ๊ธ€ Material Design์„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌํ˜„์ฒด ์ œ๊ณต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, UI์— ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • Glide url ํ˜•์‹ ์ด๋ฏธ์ง€๋ฅผ ImageView์— ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • AAC Lifecycle Live Data, Lifecycle, ViewModel ๊ณผ ๊ฐ™์€ ์ƒ๋ช…์ฃผ๊ธฐ์™€ ์—ฐ๋™๋œ ์ปดํฌ๋„ŒํŠธ๋“ค๊ณผ ํด๋ž˜์Šค ์ œ๊ณต

  • Coroutine ๋น„๋™๊ธฐ ์ž‘์—…์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ํƒ€์ž„์Šคํ…œํ”„ ์นด๋ฉ”๋ผ์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹œ๊ฐ„์˜ ๋ณ€๊ฒฝ์„ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ.

  • Activity, Fragment ktx ViewModel์„ onCreate์—์„œ ์ดˆ๊ธฐํ™” ํ•˜๋Š”๊ฒฝ์šฐ ์—ฌ๋Ÿฌ๋ฒˆ ์ƒ์„ฑํ˜น์€ ์ƒํƒœ ์†์‹ค์„ ๋ง‰๊ธฐ ์œ„ํ•ด lazy delegate ์ž‘์—…์œผ๋กœ viewModel ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์„œ ์‚ฌ์šฉ.

  • Retrofit ์•ˆ๋“œ๋กœ์ด๋“œ REST API ํ†ต์‹  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ. AsyncTask ์—†์ด Background Thread์—์„œ ์‹คํ–‰๋˜๋ฉฐ callback์„ ํ†ตํ•ด Main Thread์—์„œ์˜ UI ์—…๋ฐ์ดํŠธ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ œ๊ณต. ์„œ๋ฒ„ ํ†ต์‹ ์„ ์œ„ํ•ด ์‚ฌ์šฉ.

  • CameraX CameraX๋Š” ์นด๋ฉ”๋ผ ์•ฑ ๊ฐœ๋ฐœ์„ ๋” ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค์–ด์ง„ Jetpack ์ง€์› ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ํƒ€์ž„์Šคํ…œํ”„ ์นด๋ฉ”๋ผ ๋ถ€๋ถ„์—์„œ ์‚ฌ์šฉ.

  • Lottie Splash ๋ฐ Login ๋ฐฐ๊ฒฝ์œผ๋กœ ์‚ฌ์šฉ


๐Ÿ’ซ Team Role

  • ๐ŸŒฑ Git

    • feat : ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ
    • fix : ๋ฒ„๊ทธ ์ˆ˜์ •ํ•˜๋Š” ๊ฒฝ์šฐ
    • style : ์ƒ‰์ƒ ๋ณ€๊ฒฝ, ํฐํŠธ ๋ณ€๊ฒฝ ๋“ฑ์ด ์žˆ๋Š” ๊ฒฝ์šฐ
    • refactor : ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง ํ•˜๋Š” ๊ฒฝ์šฐ
    • upload : ํŒŒ์ผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ์šฐ
    • docs : ๋ฌธ์„œ ์ˆ˜์ •ํ•˜๋Š” ๊ฒฝ์šฐ

    Git issue template

    image

    Git PR template

    image

    Code review

    image

  • ๐ŸŒฑGithub Action & Slack bot

    Slack Bot

    • Github Action ์ž๋™ ๋นŒ๋“œ๊ฐ€ ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ.
    image
    • Github Action ์ž๋™๋นŒ๋“œ๋Š” ์„ฑ๊ณตํ–ˆ์œผ๋‚˜ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ณผ์ •์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด ๊ฒฝ์šฐ
    image
    • Github Action ์ž๋™ ๋นŒ๋“œ๋„ ์‹คํŒจํ•œ ๊ฒฝ์šฐ
    image

    Github Action

    name: MeaningAndroid Builder
    
    on:
      push:
        branches: [ develop ]
    
    defaults:
      run:
        shell: bash
        working-directory: .
    
    jobs:
      build:
        name: Generate APK
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v2
            
    
          - name: Gradle cache
            uses: actions/cache@v2
            with:
              path: |
                ~/.gradle/caches
                ~/.gradle/wrapper
              key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
              restore-keys: |
                ${{ runner.os }}-gradle-
          - name: set up JDK 1.8
            uses: actions/setup-java@v1
            with:
              java-version: 1.8
    
          - name: Change gradlew permissions
            run: chmod +x ./gradlew
    
          - name: Build with Gradle
            run: ./gradlew assembleDebug
    
          - name: On Failed, Notify in Slack
            if: ${{ failure() }}
            uses: rtCamp/action-slack-notify@v2
            env:
              SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
              SLACK_TITLE: 'nneaning/Anroid Debug build FailโŒ'
              SLACK_COLOR: '#FF5733'
              MSG_MINIMAL: true
              SLACK_MESSAGE: '์—๋Ÿฌ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”'
    
          - name: Upload APK
            if: ${{ success() }}
            uses: actions/upload-artifact@v2
            with:
              name: apk
              path: app/build/outputs/apk/debug/
    
    
      upload:
        needs: [build]
        name: upload to Slack
        runs-on: ubuntu-latest
        steps:        
          - name: download Article
            uses: actions/download-artifact@v2
            with:
              name: apk
              
          - name: Update Release apk name
            if: ${{ success() }}
            run: |
              mv app-debug.apk ๋ฏธ๋‹-Debug.apk
              echo 'apk=๋ฏธ๋‹-Debug.apk' >> $GITHUB_ENV
              
          - name: Upload APK at Slack
            if: ${{ success() }}
            run: |
              curl -X POST \
              -F file=@$apk \
              -F channels=${{secrets.SLACK_CHANNEL_ID}} \
              -H "Authorization: Bearer ${{secrets.SLACK_BOT_TOKEN}}" \
              https://slack.com/api/files.upload
              
          - name: On Success
            if: ${{ success() }}
            uses: rtCamp/action-slack-notify@v2
            env:
              SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
              SLACK_TITLE: 'nneaning/Anroid Debug build Successโœ…'
              SLACK_COLOR: '#5BFF33'
              MSG_MINIMAL: true
              SLACK_MESSAGE: 'apk ์ƒ์„ฑ ์™„๋ฃŒ! '
    
          - name: On Success but Fail
            if: ${{ failure() }}
            uses: rtCamp/action-slack-notify@v2
            env:
              SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
              SLACK_TITLE: 'nneaning/Anroid Debug build Successโœ…'
              SLACK_COLOR: '#FFF233'
              MSG_MINIMAL: true
              SLACK_MESSAGE: '๋นŒ๋“œ๋Š” ์™„๋ฃŒ ๋˜์—ˆ์œผ๋‚˜ apk์—…๋กœ๋“œ ์—๋Ÿฌ'
    


๐Ÿ’ซ meaning Tech Stack

MVC์™€ MVVM์˜ ํ˜ผํ•ฉ ์•„ํ‚คํ…์ฒ˜๋กœ ๊ฐœ๋ฐœ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • ** AAC DataBinding, ViewModel **
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
    binding.viewModel = loginViewModel
    binding.lifecycleOwner = this
    initView()
}
private val loginViewModel: LoginViewModel by viewModels {
    object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T =
            LoginViewModel(MeaningStorage.getInstance(this@LoginActivity)) as T
    }
}
  • Coroutine - ๋น„๋™๊ธฐ ์ž‘์—…

    fun runCurrentTimer() = viewModelScope.launch() {
        while (isEnableTimer) {
            _currentTime.value = SimpleDateFormat(TIME_FORMAT, Locale.KOREA)
                .format(System.currentTimeMillis())
            _currentDate.value = SimpleDateFormat(DATE_FORMAT, Locale.KOREA)
                .format(System.currentTimeMillis())
            delay(10000)
        }
    }
  • CameraX

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
        cameraProviderFuture.addListener(
            cameraProvideFutureListener(cameraProviderFuture),
            getMainExecutor()
        )
    }
    private fun cameraProvideFutureListener(
        cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
    ) = Runnable {
        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
        val preview = getCameraPreview()
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
        setImageCapture()
        try {
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
        } catch (failBindException: Exception) {
            Log.e(TAG, "Use case binding failed", failBindException)
        }
    }
    private fun getCameraPreview(): Preview = Preview.Builder()
        .build()
        .also {
            it.setSurfaceProvider(binding.cameraViewFinder.surfaceProvider)
        }
    private fun setImageCapture() {
        imageCapture = ImageCapture.Builder()
            .build()
    }
    private fun getMainExecutor() = ContextCompat.getMainExecutor(requireContext())
    private fun takePhoto() {
        val imageCapture = imageCapture ?: return
        imageCapture.takePicture(
            getMainExecutor(),
            getImageCapturedCallback()
        )
    }
    private fun getImageCapturedCallback(): TimeStampCameraCallback =
        TimeStampCameraCallback().apply {
            setOnCaptureSuccessListener { imageCaptureEvent(it) }
        }
    private fun imageCaptureEvent(image: Bitmap) {
        cameraViewModel.image = image
        cameraViewModel.isEnableTimer = false
        (requireActivity() as TimeStampCameraActivity).changeFragment(
            CameraResultFragment(),
            null
        )
    }

๐Ÿ’ซ Packaging

๐ŸŒ…meaning.morning
 โ”ฃ ๐Ÿ“‚data
 โ”ฃ ๐Ÿ“‚network
 โ”ƒ โ”ฃ ๐Ÿ“‚request
 โ”ƒ โ”ฃ ๐Ÿ“‚response
 โ”ฃ  ๐Ÿ“‚presentation
 โ”ƒ โ”ฃ ๐Ÿ“‚adapter
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚feed
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚group
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚home
 โ”ƒ โ”ฃ ๐Ÿ“‚camera
 โ”ƒ โ”ฃ ๐Ÿ“‚group
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚feed
 โ”ƒ โ”ฃ ๐Ÿ“‚home
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚card
 โ”ƒ โ”ƒ โ”ฃ ๐Ÿ“‚feed
 โ”ƒ โ”ฃ ๐Ÿ“‚login
 โ”ƒ โ”— ๐Ÿ“‚onboarding
 โ”—๐Ÿ“‚utils

๐Ÿ’ซ Main Feature Codes & Methods

โœ” sharedPreference ์‹ฑ๊ธ€ํ„ด ์ž‘์„ฑ

object๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ž‘์„ฑํ•˜๊ธฐ.

Multi-Thread Safeํ•˜๋„๋ก ๋งŒ๋“ค๊ธฐ.

SharedPreference์ง€๋งŒ ๋ณด๋‹ค ์ง๊ด€์ ์ธ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜๊ธฐ.

class MeaningStorage(context: Context) {
	/* ... */
    companion object {
        private var instance: MeaningStorage? = null
        
        fun getInstance(context: Context) = instance ?: synchronized(this) {
            instance ?: MeaningStorage(context).apply {
                instance = this
            }
        }
    }
}

โœ” TimeStamp Camera

imageimageimage

  • Camera Permission
    private fun initTimeStampCamera() {
        if (allPermissionGranted()) {
            loadCameraView()
            return
        }
        requestPermission()
    }

    private fun allPermissionGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            applicationContext,
            it
        ) == PackageManager.PERMISSION_GRANTED
    }

    private fun requestPermission() {
        ActivityCompat.requestPermissions(
            this,
            REQUIRED_PERMISSIONS,
            CameraViewModel.REQUEST_CODE_PERMISSIONS
        )
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == CameraViewModel.REQUEST_CODE_PERMISSIONS) {
            permissionResponseEvent()
        }
    }

    private fun permissionResponseEvent() {
        if (allPermissionGranted()) {
            loadCameraView()
            return
        }
        permissionDeniedEvent()
    }

    private fun permissionDeniedEvent() {
        showToast("๊ถŒํ•œ์„ ์Šน์ธํ•˜์ง€ ์•Š์œผ๋ฉด ๋‹น์‹ ์˜ ๋ฏธ๋ผํด ๋ชจ๋‹์„ ๊ธฐ๋กํ•  ์ˆ˜ ์—†์–ด์š”!")
        finish()
    }

    private fun loadCameraView() {
        changeFragment(CameraFragment())
    }

    private fun changeFragment(initFragment: Fragment) {
        val transaction = supportFragmentManager.beginTransaction()
        transaction.apply {
            replace(R.id.fragment_camera, initFragment)
            commit()
        }
    }
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
        cameraProviderFuture.addListener(
            cameraProvideFutureListener(cameraProviderFuture),
            getMainExecutor()
        )
    }

    private fun cameraProvideFutureListener(
        cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
    ) = Runnable {
        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
        val preview = getCameraPreview()
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
        setImageCapture()
        try {
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
        } catch (failBindException: Exception) {
            Log.e(TAG, "Use case binding failed", failBindException)
        }
    }

    private fun getCameraPreview(): Preview = Preview.Builder()
        .build()
        .also {
            it.setSurfaceProvider(binding.cameraViewFinder.surfaceProvider)
        }

    private fun setImageCapture() {
        imageCapture = ImageCapture.Builder()
            .build()
    }

    private fun getMainExecutor() = ContextCompat.getMainExecutor(requireContext())

    private fun takePhoto() {
        val imageCapture = imageCapture ?: return
        imageCapture.takePicture(
            getMainExecutor(),
            getImageCapturedCallback()
        )
    }

    private fun getImageCapturedCallback(): TimeStampCameraCallback =
        TimeStampCameraCallback().apply {
            setOnCaptureSuccessListener { imageCaptureEvent(it) }
        }

    private fun imageCaptureEvent(image: Bitmap) {
        cameraViewModel.image = image
        cameraViewModel.isEnableTimer = false
        /* ... */
    }

๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋งŒ๋“ค์–ด์ง„ ์นด๋ฉ”๋ผ๋ฅผ ๋ทฐ๋ชจ๋ธ์— ์ €์žฅํ•˜์—ฌ ๊ฒฐ๊ณผ ์ฐฝ์œผ๋กœ ๋„˜๊ธฐ๊ณ  ๊ฒฐ๊ณผ์ฐฝ์—์„œ๋Š” ํ•ด๋‹น ๋ทฐ๋ฅผ Bitmap์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅํ•œ๋‹ค.

class TimeStampImageCreator(private val context: Context) {
    /* ... */
    fun saveOf(viewGroup: ConstraintLayout) {
        val width = viewGroup.width
        val height = viewGroup.height
        removeViewEvent(viewGroup)
        val bitmapBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmapBuffer)
        viewGroup.draw(canvas)
        saveImage(bitmapBuffer)
    }
    
    private fun removeViewEvent(viewGroup: ConstraintLayout) {
        viewGroup.apply {
            clearFocus()
            isPressed = false
            invalidate()
        }
    }
    private fun getOutputDirectory(): File {
        val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
            File(it, context.resources.getString(R.string.app_name)).apply {
                mkdirs()
            }
        }
        return if (mediaDir != null && mediaDir.exists()) mediaDir else context.filesDir
    }

    private fun saveImage(bitmapBuffer: Bitmap) {
        photo = getPhotoFile()
        try {
            val outputStream = FileOutputStream(photo)
            bitmapBuffer.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
            outputStream.close()
            galleryAddPicture()
        } catch (errorMessage: FileNotFoundException) {
            errorMessage.stackTrace
        } catch (errorMessage: IOException) {
            errorMessage.stackTrace
        } finally {
            bitmapBuffer.recycle()
        }
    }

    private fun getPhotoFile() = File(
        getOutputDirectory(),
        SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss",
            Locale.KOREA
        ).format(System.currentTimeMillis()) + ".jpeg"
    )
}

๋งŒ๋“  ํŒŒ์ผ์€ ๊ธ€์“ฐ๊ธฐ ํ™”๋ฉด์œผ๋กœ ๋„˜๊ธด๋‹ค.

โœ” MyFeedPictureAdapter

์•„์ดํ…œ ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋ถ„๋ฆฌ.

class MyFeedPictureAdapter : RecyclerView.Adapter<MyFeedPictureAdapter.MyFeedPictureViewHolder>() {
    var data = mutableListOf<MyFeedPictureData>()
    private lateinit var itemClickListener : ItemClickListener
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyFeedPictureViewHolder {
        val binding = FeedItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MyFeedPictureViewHolder(binding)
    }
    override fun getItemCount(): Int {
        return data.size
    }
    override fun onBindViewHolder(holder: MyFeedPictureViewHolder, position: Int) {
        holder.onBind(data[position])
        holder.itemView.setOnClickListener {
            itemClickListener.onClick(it,position)
        }
    }
    fun submitData(list : List<MyFeedPictureData>){
        data.addAll(list)
        notifyDataSetChanged()
    }
    class MyFeedPictureViewHolder(val binding: FeedItemListBinding) : RecyclerView.ViewHolder(binding.root) {
        fun onBind(data: MyFeedPictureData) {
            binding.feedItemList = data
        }
    }
    interface ItemClickListener{
        fun onClick(view : View, position: Int)
    }
    fun setItemClickListener(itemClickListener: ItemClickListener){
        this.itemClickListener = itemClickListener
    }
}

๐Ÿ’ซ Layout ๊ด€๋ จ

  • Layout ์‚ฌ์šฉ

    ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์œผ๋กœ ์‚ฌ์šฉ์œผ๋กœ ๋ชจ๋“  ๋ทฐ์˜ ์ตœ์ƒ์œ„๊ฐ€ Layout ํƒœ๊ทธ ์•„๋ž˜ ์žˆ์Œ

  • coordinatorlayout, NestedScrollView ์‚ฌ์šฉ ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ behavior๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ทฐ์˜ ๋ณ€๊ฒฝ์„ ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉ.

    - fragment_group.xml
    - activity_my_feed_main.xml
    - activity_group_settting.xml
    
  • ๋‹จ์ˆœ ๋„ํ˜• ์—์…‹ - ์บ˜๋ฆฐ๋” ๋ทฐ ์•„๋ž˜ ์›

image

radius ํ™•์ธ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜์—ฌ ๋””์ž์ด๋„ˆ์—๊ฒŒ ์š”์ฒญํ›„ ์—์…‹์œผ๋กœ ๋ฐ›๊ธฐ๋กœํ•จ

- HomeFragment
  • ์ ˆ๋Œ€ ํฌ๊ธฐ ์ง€์ •

    - feed_item_list.xml
    - dialog_group_recycler.xml
    - dialog_group_detail.xml
    - fragment_home.xml
    
    • feed_item_list : ํ”ผ๋“œ ์•„์ดํ…œ์œผ๋กœ ๋“ค์–ด์˜ฌ ์‚ฌ์ง„ ํฌ๊ธฐ๊ฐ€ ๊ธฐ๊ธฐ๋ณ„๋กœ ๋‹ค๋ฅผ ๊ฒฝ์šฐ๋ฅผ ๋”ฐ๋ผ ์ ˆ๋Œ€ ํฌ๊ธฐ ์ง€์ •
    • dialog : ํ™”๋ฉด ๋น„์œจ์— ๋”ฐ๋ผ๊ฐ€ ์•„๋‹Œ ๋‹ค์ด์–ผ๋กœ๊ทธ ์ฐฝ์˜ ํฌ๊ธฐ ๊ณ ์ •์„ ์œ„ํ•ด์„œ ์‚ฌ์šฉ
    • fragement_home.xml : > ๋ชจ์–‘ ์—์…‹ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ์ž‘๋‹ค๋Š” ์š”์ฒญ์— ์ ˆ๋Œ€ํฌ๊ธฐ๋กœ ์•ฝ๊ฐ„ ํฌ๊ธฐ ์ฆ๊ฐ€ ์ง€์ •.

About

๐ŸŽดํ•œ ๋—์— 5์–ต์„ ์•ˆ๋“œ๋กœ๋ฏธ๋‹? ์ซ„๋ฆฌ๋ฉด ์ฃผ๋ฌด์‹œ๋˜์ง€...๐Ÿ’ซ


Languages

Language:Kotlin 100.0%