[요구사항]
- 사용자 프로필 변경 시 이미지 선택
- 갤러리에서 이미지를 사용하는 것이 아닌 커스텀 갤러리 구현
- 이미지를 불러와서 슬라이드 뷰에 적용
1. 갤러리 사용 권한 받기
- 권한 설정에 TedPerMission을 사용한다.
- 간편하게 사용자 권한 코드 작성 가능
implementation 'io.github.ParkSangGwon:tedpermission:2.3.0'
- ManiFest에 추가
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2. 슬라이딩 패널 라이브러리 추가
//slide bar
implementation 'com.sothree.slidinguppanel:library:3.4.0'
- 슬라이딩 패널의 주요 요소
android:gravity="bottom"
app:umanoDragView="@id/slide_layout_in_update_profile"
app:umanoOverlay="true"
app:umanoPanelHeight="0dp"
- gravity : 필수 - 패널 나타나는 위치
- umanoDragView : 숨겨질 뷰 (서브 뷰 )
- umanoOverlay : 배경 레이아웃이 공간을 차지하는지 설정 (false : 슬라이드 뷰가 기존 뷰를 밀고 올라옴)
- umanoPanelHeight : 숨겨져 있을 때의 높이
- 구체적인 레이아웃 (메인 뷰)
제일 하단의 frame Layout이 슬라이딩 뷰를 담당
<com.sothree.slidinguppanel.SlidingUpPanelLayout 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/slide_frame_in_update_profile"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
app:umanoDragView="@id/slide_layout_in_update_profile"
app:umanoOverlay="true"
app:umanoPanelHeight="0dp"
tools:context=".UIFragment.ProfileUpdateFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="@dimen/fab_margin">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/item_mid_50_height"
android:layout_marginBottom="@dimen/fab_margin"
android:orientation="horizontal">
<ImageView
android:id="@+id/back_button"
android:layout_width="@dimen/item_mid_40_height"
android:layout_height="@dimen/item_mid_40_height"
android:background="@drawable/circle_border"
android:src="@drawable/ic_baseline_arrow_back_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/update_profile"
android:textColor="@color/color_type1"
android:textSize="@dimen/item_height_big_text_size_35"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/user_image"
android:layout_width="@dimen/item_height_big_150"
android:layout_height="@dimen/item_height_big_150"
android:layout_margin="@dimen/main_margin_10"
android:background="@drawable/circle"
android:padding="@dimen/main_margin" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/item_height"
android:layout_margin="@dimen/fab_margin_12dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="@dimen/main_margin"
android:text="@string/enter_your_new_id"
android:textColor="@color/strong_color"
android:textSize="@dimen/item_height_big_text_size"
android:textStyle="bold" />
<EditText
android:id="@+id/insert_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin_12dp"
android:gravity="center"
android:textSize="@dimen/item_height_big_21_text_size"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/item_height"
android:layout_margin="@dimen/fab_margin_12dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="@dimen/main_margin"
android:text="@string/enter_your_new_comment"
android:textColor="@color/strong_color"
android:textSize="@dimen/item_height_big_text_size"
android:textStyle="bold" />
<EditText
android:id="@+id/insert_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin_12dp"
android:gravity="center"
android:textSize="@dimen/item_height_big_21_text_size"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/commit_button"
style="@style/ButtonText_big"
android:layout_width="@dimen/item_we_300"
android:layout_height="@dimen/item_height_big_70"
android:layout_gravity="center"
android:layout_margin="@dimen/main_margin_10"
android:text="commit" />
</LinearLayout>
<FrameLayout
android:id="@+id/slide_layout_in_update_profile"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.sothree.slidinguppanel.SlidingUpPanelLayout>
3. 슬라이딩 레이아웃을 Fragment로 구현
class SelectPicFragment(private val mainFrame : SlidingUpPanelLayout) : Fragment()
프로필을 변경하는 메인 뷰에서 위에서 선언한 프레임 레이아웃을 프래그먼트로 변경
selectPicFragment= SelectPicFragment(binding.slideFrameInUpdateProfile)
//슬라이드 레이아웃 view 설정
selectPicFragment?.let {fragment->
mainActivity.frManger.beginTransaction()
.replace(R.id.slide_layout_in_update_profile,fragment)
.commit()
}
4. 슬라이딩 레이아웃 뷰 작성
이미지를 보여줄 리싸이클러 뷰와 어댑터 필요
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:clickable="true"
android:background="@color/white"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".UIFragment.SelectPicFragment"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/item_height_big_80">
<TextView
android:id="@+id/cancel_button"
android:textSize="@dimen/item_height_big_21_text_size"
android:text="@string/back"
android:textStyle="bold"
android:gravity="center"
android:layout_marginStart="@dimen/item_mid_height_20dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/upload_button"
android:textSize="@dimen/item_height_big_21_text_size"
android:text="@string/commit"
android:textStyle="bold"
android:gravity="center"
android:layout_marginEnd="@dimen/item_mid_height_20dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/line"
android:background="@color/color_type1"
android:layout_width="match_parent"
android:layout_height="1dp"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="@dimen/item_we_300">
<ImageView
android:layout_gravity="center"
android:id="@+id/select_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>
<FrameLayout
android:background="@color/color_type1"
android:layout_width="match_parent"
android:layout_height="1dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/image_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
tools:listitem="@layout/item_one_pic"
android:orientation="vertical"
/>
</LinearLayout>
SelectPicAdapter로 이미지를 나타내고 클릭 이벤트를 적용
- 이미지 Uri를 리스트로 받아서 데이터를 보여줌
- 클릭 이벤트 발생 시 선택 아이콘을 나타내고 uri를 어댑터를 선언한 곳으로 전달
- 0번째 인덱스의 사진을 클릭할 경우, 사진을 촬영하라는 신호인 camera! : String 전달
class SelectPicAdapter(
private val mainActivity: MainActivity,
private var itemList: ArrayList<Uri>,
) : RecyclerView.Adapter<SelectPicAdapter.SelectPicViewHolder>() {
private lateinit var binding: ItemOnePicBinding
private var onItemClickListener :OnItemClickListener?=null
private var selectedPicNum = -1
interface OnItemClickListener{
fun onItemClick(imageUri:Uri){
}
}
fun setOnItemClickListener(listener: OnItemClickListener){
this.onItemClickListener = listener
}
inner class SelectPicViewHolder(private val binding: ItemOnePicBinding) :
RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(){
val item = itemList[absoluteAdapterPosition]
//카메라 뷰 = 0
if (absoluteAdapterPosition==0){
binding.checkImage.setBackgroundResource(R.drawable.baseline_camera_alt_24)
binding.checkBox.setImageResource(0) //카메라는 체크 박스 제거
}else {
Glide.with(mainActivity)
.load(item)
.into(binding.checkImage)
if (selectedPicNum == absoluteAdapterPosition) {
binding.setCheck()
} else {
binding.setUnCheck()
}
}
binding.root.setOnClickListener {
//카메라 세팅
if (absoluteAdapterPosition ==0){
onItemClickListener?.onItemClick("camera!".toUri())
//재 클릭 시 선택 종료
} else if (absoluteAdapterPosition == selectedPicNum) {
onItemClickListener?.onItemClick("".toUri()) //선택 uri 전달
selectedPicNum = -1
//이미지 클릭 시 uri 전달
} else {
onItemClickListener?.onItemClick(item) //선택 uri 전달
selectedPicNum = absoluteAdapterPosition
}
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectPicViewHolder {
binding = ItemOnePicBinding.inflate(LayoutInflater.from(mainActivity),parent,false)
return SelectPicViewHolder(binding)
}
override fun getItemCount() = itemList.size
override fun onBindViewHolder(holder: SelectPicViewHolder, position: Int) {
holder.bind()
}
private fun ItemOnePicBinding.setCheck() =
checkBox.setBackgroundResource(R.drawable.baseline_check_circle_24)
private fun ItemOnePicBinding.setUnCheck() =
checkBox.setBackgroundResource(R.drawable.circle)
}
5. 슬라이딩 레이아웃 프래그먼트 작성
- 이미지 리스트를 보여주고 , 사용자 권한을 요청하는 코드 작성
- 이미지 선택 시 main view로 Uri 전송
- 권한 확인 코드
SelectPicAdapter : 위에서 작성한 갤러리에서 불러온 사진들을 보여주기 위한 어댑터
- getGallery() 함수를 통해서 이미지 리스트를 가져옴 (아래에 참조)
- 권한이 확인되면 어댑터를 생성하고 이미지 리스트를 보여준다.
- 위의 어댑터에서 생성한 클릭 이벤트를 통해서 Uri를 전달받고 이미지를 나타냄
- 만약에 사진 촬영을 하라는 "camera!" 인자가 올 경우 사진 촬영 함수인 openCamera() 실행 (아래에 참조)
- 권한 요청이 실패할 경우 메시지 전달
private lateinit var permissionListener : PermissionListener
//권한 확인
permissionListener = object : PermissionListener{
override fun onPermissionGranted() {
val adapter = SelectPicAdapter(mainActivity,getGallery())
binding.imageRecycler.adapter = adapter
.apply {
setOnItemClickListener(object : SelectPicAdapter.OnItemClickListener{
override fun onItemClick(imageUri: Uri) {
if (imageUri.toString()==""){
binding.selectImage.setImageResource(0)
lastUri = imageUri
}else if (imageUri.toString()=="camera!"){
//카메라 실행 코드
openCamera()
}else{
Glide.with(mainActivity)
.load(imageUri)
.into(binding.selectImage)
lastUri = imageUri
}
}
})
}
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
Toast.makeText(mainActivity,"권한 필요", Toast.LENGTH_SHORT)
.show()
}
}
제작한 권한을 적용하는 코드 (반드시 onCreate() or onCreated() 내부에서 실행되어야 함)
- 카메라 촬영 권한, 사진을 읽고 쓰는 권한 적용
- 권한 허용을 위해서 사용자에게 아래와 같은 메시지 나타냄
//권한 적용
TedPermission.with(mainActivity)
.setPermissionListener(permissionListener)
.setPermissions(
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
).check()
6. getGallery() : 사용자 이미지 리스트를 불러오는 함수
- 0번째에는 카메라 촬영 옵션을 넣기 위해서 빈 값을 넣어줌
- 커서를 이동하면서 이미지리스트 작성
private fun getGallery() :ArrayList<Uri>{
val picList = ArrayList<Uri>()
picList.add(Uri.parse("")) //첫번째는 카메라
val uriExternal : Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
var columnIndexId : Int
val cursor = mainActivity.contentResolver.query(
uriExternal,null,null,null,
MediaStore.Images.ImageColumns.DATE_TAKEN+" DESC"
)
if (cursor!=null){
while (cursor.moveToNext()){
columnIndexId = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val imageId = cursor.getLong(columnIndexId)
val uriImage = Uri.withAppendedPath(uriExternal,""+imageId)
picList.add(uriImage)
}
cursor.close()
}
return picList
}
7. openCamera() : 사용자 카메라 촬영을 진행하는 함수
- 사진 촬영을 진행하고, Uri를 전달받아서 선택한 이미지로 적용
private val startForResult = registerForActivityResult(ActivityResultContracts
.StartActivityForResult()) {}
private fun openCamera(){
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
createImageUri(newFileName())?.let { uri->
createUri = uri
intent.putExtra(MediaStore.EXTRA_OUTPUT,createUri)
startForResult.launch(intent)
createUri?.let {
Glide.with(mainActivity)
.load(createUri)
.into(binding.selectImage)
lastUri =createUri
}
}
}
새로운 파일이 생성되었으므로 새로운 이미지 파일을 생성하는 함수도 작성
//새 파일 이름 생성
@SuppressLint("SimpleDateFormat")
private fun newFileName() : String {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmSS")
return "${sdf.format(System.currentTimeMillis())}.jpeg"
}
//이미지 파일 생성 함수
private fun createImageUri(filename : String) : Uri?{
val values = ContentValues()
values.put(MediaStore.Images.Media.DISPLAY_NAME,filename)
values.put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg")
return mainActivity.contentResolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values)
}
7. 클릭 이벤트 작성
- 서브 뷰 (슬라이딩 뷰)를 불러온 메인 뷰로 선택한 Uri를 전달하기 위해서 클릭 이벤트를 적용
private var onItemClickListener : OnItemClickListener?= null
interface OnItemClickListener{
fun onItemClick(lastUri:Uri?){}
}
fun setOnItemClickListener(listener: OnItemClickListener){
this.onItemClickListener = listener
}
업로드 버튼 클릭 시 슬라이딩 뷰를 종료하고,
클릭 이벤트를 발생시켜서 메인 뷰로 데이터 전달
binding.uploadButton.setOnClickListener {
mainFrame.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED
onItemClickListener?.onItemClick(lastUri)
}
메인 뷰에서 클릭 이벤트 처리 : 선택 Uri를 받아서 메인 뷰의 이미지에 적용
// 슬라이드로 이미지 변경 프래그먼트 처리
// 클릭 이벤트 시 이미지 가져옴
selectPicFragment= SelectPicFragment(binding.slideFrameInUpdateProfile)
.apply {
setOnItemClickListener(object :SelectPicFragment.OnItemClickListener{
override fun onItemClick(lastUri: Uri?) {
imageUri =
if (lastUri.toString()!=""){
lastUri
}else{
Uri.parse(mainActivity.viewModel.myProfile.userImage)
}
Glide.with(mainActivity)
.load(imageUri)
.into(binding.userImage)
}
})
}
실행 화면