We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
Contact:[email protected]
GitHub:jinsu4755
GitHub:hyooosong
Contact: [email protected]
GitHub:LEE-HYUNGJUN
- 타임스템프 카메라 - 로그인 - 온보딩 - 서버 연결 로직 구현
- 그룹 탭 - 캘린더 뷰 - sharedPreferences 싱글톤 객체 구현
- 홈 메인페이지 - 카드 뷰 애니메이션 - 마이 피드, 그룹 피드 - 피드 상세보기 뷰
meaning
기상시간이 달라진다면, 당신도 변할 수 있습니다.
‘내’가 눈 뜨는 시간이 아닌, ‘해’가 뜨는 시간부터 하루를 시작하는 미라클 모닝.
미닝을 통해 미라클 모닝에 도전하며 당신만의 의미있는 아침을 만들어 나가보세요.
일찍 일어나는 습관으로 하루를 길게 보내면, 성장의 발판을 마련할 수 있습니다.
미닝과 함께 체계적인 계획을 세우고 이를 규칙적으로 실천하면서 성취감을 얻어보세요.
성장지향적인 그룹원과 목표를 공유한다면 우리는 함께, 더 멀리 갈 수 있습니다.
Material Design Component 구글 Material Design을 쉽게 사용할 수 있는 구현체 제공 라이브러리, UI에 사용하였습니다.
Glide url 형식 이미지를 ImageView에 표시하기 위해 사용하였습니다.
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 배경으로 사용
feat
fix
style
refactor
upload
docs
MVC와 MVVM의 혼합 아키텍처로 개발 하였습니다.
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 ) }
🌅meaning.morning ┣ 📂data ┣ 📂network ┃ ┣ 📂request ┃ ┣ 📂response ┣ 📂presentation ┃ ┣ 📂adapter ┃ ┃ ┣ 📂feed ┃ ┃ ┣ 📂group ┃ ┃ ┣ 📂home ┃ ┣ 📂camera ┃ ┣ 📂group ┃ ┃ ┣ 📂feed ┃ ┣ 📂home ┃ ┃ ┣ 📂card ┃ ┃ ┣ 📂feed ┃ ┣ 📂login ┃ ┗ 📂onboarding ┗📂utils
object를 사용하지 않고 작성하기. Multi-Thread Safe하도록 만들기. 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 } } } }
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" ) }
만든 파일은 글쓰기 화면으로 넘긴다.
아이템 클릭 이벤트를 인터페이스로 분리.
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 태그 아래 있음
coordinatorlayout, NestedScrollView 사용 스크롤 이벤트 발생시 behavior를 이용하여 뷰의 변경을 하기 위해서 사용.
- fragment_group.xml - activity_my_feed_main.xml - activity_group_settting.xml
단순 도형 에셋 - 캘린더 뷰 아래 원
radius 확인이 불가능하여 디자이너에게 요청후 에셋으로 받기로함
- HomeFragment
절대 크기 지정
- feed_item_list.xml - dialog_group_recycler.xml - dialog_group_detail.xml - fragment_home.xml
The text was updated successfully, but these errors were encountered:
Sorry, something went wrong.
jinsu4755
LEE-HYUNGJUN
hyooosong
No branches or pull requests
🌜 미라클 모닝으로 시작하는 당신의 의미있는 아침, meaning 🌝
👇 meaning_Android 👇
🎴 About US
Contact:[email protected]
GitHub:jinsu4755
Contact:[email protected]
GitHub:hyooosong
Contact: [email protected]
GitHub:LEE-HYUNGJUN
🦂진수
🎅 효송
👨 👧 형준
🏆 Meeting Log
🎴 미닝언즈 안드로미닝 회의록
📝 List
1. [Service]
2. [Andromeaning Development Environment]
3. [Work Flow]
4. [Dependencies]
5. [Team Role]
6. [meaning Tech Stack]
7. [Packaging]
8. [Main Feature Codes & Methods]
💫 Service about
meaning
모든 것은 바뀔 수 있고 나 역시 무언가를 바꿀 수 있습니다.
기상시간이 달라진다면, 당신도 변할 수 있습니다.
‘내’가 눈 뜨는 시간이 아닌, ‘해’가 뜨는 시간부터 하루를 시작하는 미라클 모닝.
미닝을 통해 미라클 모닝에 도전하며 당신만의 의미있는 아침을 만들어 나가보세요.
일찍 일어나는 습관으로 하루를 길게 보내면, 성장의 발판을 마련할 수 있습니다.
미닝과 함께 체계적인 계획을 세우고 이를 규칙적으로 실천하면서 성취감을 얻어보세요.
성장지향적인 그룹원과 목표를 공유한다면 우리는 함께, 더 멀리 갈 수 있습니다.
💫 Development Environment
💫 Work Flow
💫 Dependencies
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
🌱 Andromeaning Conventions
🌱 Andromeaning Coding Style
🌱 Code Review Guideline
🌱 Git
feat
: 새로운 기능 추가하기fix
: 버그 수정하는 경우style
: 색상 변경, 폰트 변경 등이 있는 경우refactor
: 코드 리팩토링 하는 경우upload
: 파일 생성하는 경우docs
: 문서 수정하는 경우💫 meaning Tech Stack
Coroutine - 비동기 작업
CameraX
💫 Packaging
💫 Main Feature Codes & Methods
✔ sharedPreference 싱글턴 작성
✔ TimeStamp Camera
다음과 같이 만들어진 카메라를 뷰모델에 저장하여 결과 창으로 넘기고 결과창에서는 해당 뷰를 Bitmap으로 변환하여 저장한다.
만든 파일은 글쓰기 화면으로 넘긴다.
✔ MyFeedPictureAdapter
아이템 클릭 이벤트를 인터페이스로 분리.
💫 Layout 관련
Layout 사용
데이터 바인딩으로 사용으로 모든 뷰의 최상위가 Layout 태그 아래 있음
coordinatorlayout, NestedScrollView 사용
스크롤 이벤트 발생시 behavior를 이용하여 뷰의 변경을 하기 위해서 사용.
단순 도형 에셋 - 캘린더 뷰 아래 원
radius 확인이 불가능하여 디자이너에게 요청후 에셋으로 받기로함
절대 크기 지정
The text was updated successfully, but these errors were encountered: