Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

리드미 양식 #2

Open
hyooosong opened this issue Jan 2, 2021 · 1 comment
Open

리드미 양식 #2

hyooosong opened this issue Jan 2, 2021 · 1 comment
Assignees

Comments

@hyooosong
Copy link
Member

hyooosong commented Jan 2, 2021


🌜 미라클 모닝으로 시작하는 당신의 의미있는 아침, meaning 🌝


👇 meaning_Android 👇



🎴 About US

진수 효송 형준

Contact:[email protected]

GitHub:jinsu4755

Contact:[email protected]

GitHub:hyooosong

Contact: [email protected]

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



💫 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 : > 모양 에셋 크기가 너무 작다는 요청에 절대크기로 약간 크기 증가 지정.
@jinsu4755
Copy link
Member

미닝  서비스 아이콘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants