제일 막막했던 기능..!
하지만 MVP 중 하나여서 무척 중요한 기능이다!!
그래서 미루고 미루다 이미지 저장 기능을 구현한김에 같이 해버리자!해서 시작했다
결국 하루종일 저것만 구현했ㅋㅋㅋㅋㅋ
하지만 그마저도 완벽하지 않은ㅎㅎ;;
후... 저 배경 어케 없애냐....😭😫 (음.. 이제와서 든 생각이지만 스토리 올릴때 자동으로 배경 설정이 되니, 배경을 따로 설정해줘야하나봄????)
-> 수정완료!!
흰색 배경화면을 비트맵으로 나타내는 코드 작성,,
그 배경 비트맵을 uri로 반환,,이건 레이아웃뷰를 uri로 반환하는 코드에서 같이!!
마지막으로 배경 이미지(bgUri)를 인텐트의 데이터로 설정하고 인스타그램에 전달해줌
배경 자산 : 말 그대로 배경. 움직이지 않는다.
스티커 자산 : 움직이거나 크기를 조절할 수 있는 스티커이다. 앨범아트, 가수명, 노래제목 등이 들어간다.
인텐트
인텐트는 안드로이드 애플리케이션 간에 작업을 수행하도록 메시지를 전송하는 데 사용되는 구조이다.
주로 액티비티 시작, 서비스 시작, 브로드캐스트 메시지 전송 등과 같은 다양한 작업을 수행하는 데에 활용
인텐트는 애플리케이션 간의 통신을 용이하게 만들어주는 중요한 컴포넌트
여기서 인텐트는 Instagram 앱에게 Instagram Story에 이미지를 추가하도록 지시하는 데 사용했다. 구체적으로는 Instagram의 Story 기능을 활용하여 사용자가 생성한 이미지를 배경으로 하여 스티커 형태로 추가하는 작업을 수행
.
인텐트에 설정된 정보들은 Instagram 앱에게 전달되며, Instagram 앱은 이 정보들을 활용하여 Story에 이미지를 추가하고, 그 결과를 사용자에게 표시하게 된다.
따라서 이 코드에서의 인텐트는 Instagram 앱과의 상호작용을 통해 Instagram Story에 이미지를 추가하는 역할!!!
이 기능 또한 세개의 프래그먼트에서 데이터를 주고받고 통신해야하니,
인터페이스보다 앞서 배운 같은 뷰모델을 사용함
class SharedViewModel : ViewModel(){
// 프로필 레이아웃에 대한 View를 저장하는 LiveData
//여기서 나는 frontprifilefragment에서 레이아웃뷰를 가져와 저장해야함
val profileLayoutLiveData = MutableLiveData<View>()
// 비트맵 저장 여부를 나타내는 LiveData
val storeBitmap = MutableLiveData<Boolean>()
//프로필 레이아웃을 저장하고, storeBitmap 값을 true로 설정하여 비트맵을 저장하도록 지시
//@param profileLayout : 저장할 프로필 레이아웃 View
fun storeProfileLayout(profileLayout: View) {
//저장할 프로필 레이아웃을 위에서 선언한 frontprifilefragment에 넣음
profileLayoutLiveData.value = profileLayout
//프로필 레이아웃 저장하면 true로
setStoreBitmap(true)
}
//storeBitmap LiveData 값을 설정하는 메서드.
fun setStoreBitmap(value: Boolean) {
// storeBitmap LiveData 값을 설정
storeBitmap.value = value
}
}
class FrontProfileFragment : Fragment(){
private lateinit var sharedViewModel: SharedViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//ViewModel을 생성하고 가져오는 부분
//ViewModelProvider는 ViewModel을 생성하고 관리하는 클래스
//ViewModelProvider는 ViewModel 객체를 얻기 위한 메서드인 get 메서드를 제공
//requireActivity():프래그먼트와 활동 간의 통신이나 활동 범위에서 사용하는 리소스에 접근
sharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
sharedViewModel.storeProfileLayout(binding.profileLayout)
//LiveData를 관찰하여 데이터의 변경을 감지하고, 변경이 발생할 때마다 특정 동작을 수행
//LiveData를 관찰하는 메서드로, 데이터가 변경될 때 수행할 동작을 정의
sharedViewModel.profileLayoutLiveData.observe(viewLifecycleOwner) {
if (sharedViewModel.storeBitmap.value == true) {
// 프로필 레이아웃을 비트맵으로 저장하는 로직 수행
val bitmap = getViewBitmap(it)
val filePath = getSaveFilePathName()
bitmapFileSave(bitmap, filePath)
Log.d("bitmap", "success")
}
}
}
FileProvider 추가
앱에서는 Content Uri를 사용해야하는데
FileProvider는 Content Uri를 제공하므로 FileProvider이용해야함
FileProvider 클래스의 getUriForFile() 메서드를 사용!!
=> getUriForFile(context, {Manifest에서 정의한 authorities}, File객체)
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.aboutme.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
위의 manifest에서 이용된 res\xml에 file_paths.xml 추가
이는 공유 가능한 디렉토리를 설정하는 것
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="storage/emulated" path="." />
</paths>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
다이얼로그에서 '인스타공유' 버튼 눌렀을때 인스타 스토리로 뷰 이미지 공유
class BottomSheet2 : DialogFragment() {
// SharedViewModel 인스턴스를 나타내는 변수
private lateinit var sharedViewModel: SharedViewModel
// onViewCreated: Fragment가 뷰를 만들고 나서 호출되는 메서드
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// SharedViewModel 초기화
// ViewModelProvider를 사용하여 ViewModel 인스턴스 생성 또는 가져오기
sharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
// "shareBottomSheet2InstargramBtn" 버튼 클릭 시 동작 설정
binding.shareBottomSheet2InstargramBtn.setOnClickListener {
// Android 버전이 Q (Android 10) 이상인 경우
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//let : profileLayoutLiveData의 현재 값이 null이 아닌 경우에만 실행되는 블록
//sharedViewModel.profileLayoutLiveData.value의 의미 : profileLayoutLiveData에 저장된 LiveData의 현재 값에 접근
//따라서 여기서 profileLayout은 frontprofilefragment에서 바인딩한 명함뷰 레이아웃
sharedViewModel.profileLayoutLiveData.value?.let { profileLayout ->
// 프로필 레이아웃을 Bitmap으로 변환
val viewBitmap = getViewBitmap(profileLayout)
// Android Q 이상에서 이미지를 저장하고 Uri를 반환하는 메서드 호출
val viewUri = saveImageOnAboveAndroidQ(viewBitmap)
val bgBitmap = drawBackgroundBitmap()
val bgUri = saveImageOnAboveAndroidQ(bgBitmap)
//인스타 공유
instaShare(bgUri, viewUri)
Log.d("insta!!", "success")
}
}
//안드로이드 버전이 10미만일때
else {
// 외부 저장소 읽기 권한 체크
val readPermission = ActivityCompat.checkSelfPermission(
requireContext(),
//Manifest의 외부저장소 read permission
Manifest.permission.READ_EXTERNAL_STORAGE
)
// 권한이 부여되지 않은 경우 권한 요청
if (readPermission != PackageManager.PERMISSION_GRANTED) {
val requestReadExternalStorageCode = 2
val permissionStorage = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE
)
ActivityCompat.requestPermissions(
requireActivity(),
permissionStorage,
requestReadExternalStorageCode
)
}else {
// 이미 권한이 있는 경우에 수행할 동작:(안드로이드10이상에서의 코드와 같음)
sharedViewModel.profileLayoutLiveData.value?.let { profileLayout ->
// 프로필 레이아웃을 Bitmap으로 변환
val viewBitmap = getViewBitmap(profileLayout)
// Android Q 미만에서 이미지를 저장하고 Uri를 반환하는 메서드 호출
val viewUri = saveImageOnAboveAndroidQ(viewBitmap)
val bgBitmap = drawBackgroundBitmap()
val bgUri = saveImageOnUnderAndroidQ(bgBitmap)
instaShare(bgUri,viewUri)
}
}
}
// BottomSheet 닫기
dismiss()
}
}
// saveImageOnAboveAndroidQ: Android Q (Android 10) 이상에서 이미지를 저장하고 Uri를 반환하는 메서드
//비트맵을 받아서 외부 저장소의 DCIM/ImageSave 경로에 PNG 형식으로 이미지를 저장
//@RequiresApi 어노테이션 : 해당 API 레벨 이상에서만 사용 가능한 기능을 지정할 수 있음
@RequiresApi(Build.VERSION_CODES.Q)
//비트맵을 매개변수로 받고 Uri반환/실패한 경우 null을 반환
private fun saveImageOnAboveAndroidQ(bitmap: Bitmap): Uri? {
// 현재 시간을 기반으로 고유한 파일 이름 생성
val fileName = System.currentTimeMillis().toString() + ".Png"
// ContentValues를 사용하여 이미지 정보 설정
// ContentValues는 데이터베이스에 삽입할 데이터를 담는데 사용되는 컨테이너
val contentValues = ContentValues().apply {
// 저장할 디렉토리 경로 지정
put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/ImageSave")
// 이미지 파일의 표시 이름 설정 (파일명)
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
// 이미지 파일의 MIME 타입 설정 (이미지 형식)
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
// 이미지 파일이 사용 중인지 여부 설정 (1: 사용 중, 0: 사용 안 함)
// 다른 곳에서 이 데이터를 요구하면 무시하라는 의미로, 해당 저장소를 독점할 수 있다.
put(MediaStore.Images.Media.IS_PENDING, 1)
}
// Uri를 통해 이미지 저장 요청
//requireContext().contentResolver.insert를 사용하여
//외부 저장소의 MediaStore.Images.Media.EXTERNAL_CONTENT_URI에 이미지 파일 정보를 삽입
val uri = requireContext().contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
try {
if (uri != null) {
//openFileDescriptor 메서드는 지정된 URI에 대한 파일 디스크립터를 여는것
//"w"는 쓰기 모드
//열린 파일 디스크립터를 사용하여 이미지 파일에 대한 작업을 수행할 수 있게됨
val image = requireContext().contentResolver.openFileDescriptor(uri, "w", null)
if (image != null) {
// 이미지 파일을 쓰기 위한 FileOutputStream 생성
//fos를 사용하여 이미지 파일에 데이터를 쓸 수 있게됨
val fos = FileOutputStream(image.fileDescriptor)
// 비트맵을 PNG 형식으로 압축하여 저장
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
//항상 스트림 닫아주기
fos.close()
// ContentValues를 초기화하여 다시 사용 가능하게 만듦
contentValues.clear()
// IS_PENDING 값을 0으로 변경하여 이미지 저장 완료를 알림
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
requireContext().contentResolver.update(uri, contentValues, null, null)
}
}
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
}
//저장된 이미지 uri반환
return uri
}
// instaShare: Instagram에 이미지를 공유하는 메서드
fun instaShare(bgUri: Uri?, viewUri: Uri?) {
// Instagram으로 전달할 stickerAssetUri 설정
//문자열로 된 Uri를 Uri 객체로 파싱
val stickerAssetUri = Uri.parse(viewUri.toString())
// Instagram에게 인식되는 sourceApplication을 정의합니다.
val sourceApplication = "com.example.aboutme"
// Instagram 공유를 위한 인텐트 생성
//이 인텐트는 인스타그램 앱에게 스토리에 이미지를 추가하는 요청을 전달
val intent = Intent("com.instagram.share.ADD_TO_STORY")
// 인텐트에 sourceApplication 추가
intent.putExtra("source_application", sourceApplication)
// 인텐트의 타입을 이미지/png로 설정
type = "image/png"
// 이미지 데이터 및 타입을 설정
setDataAndType(bgUri, "image/png")
// stickerAssetUri(사용자가 생성한 이미지)를 interactive_asset_uri로 인텐트에 추가
// 이것이 스티커 또는 추가적인 인터랙티브 요소로 사용됨
intent.putExtra("interactive_asset_uri", stickerAssetUri)
}
// Instagram 패키지에 대한 읽기 권한 부여
// Activity에게 URI에 대한 읽기 권한을 부여
//requireActivity() 함수: 현재 Fragment가 속한 Activity를 반환
requireActivity().grantUriPermission(
"com.instagram.android", stickerAssetUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
)
// Instagram 패키지에 대한 읽기 권한 부여
requireActivity().grantUriPermission(
"com.instagram.android", viewUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
)
// 생성한 인텐트를 실행
try {
// Instagram 앱 실행
this.startActivity(intent)
} catch (e: ActivityNotFoundException) {
// Instagram 앱이 설치되어 있지 않은 경우 예외 처리
Toast.makeText(
requireContext().applicationContext,
"인스타그램 앱이 존재하지 않습니다.",
Toast.LENGTH_SHORT
).show()
// 웹 브라우저에서 Instagram 웹 페이지 열기
val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.instagram.com/"))
startActivity(webIntent)
}
//이미지를 공유한 후에 이미지 파일을 삭제
try {
// 이미지를 저장하고 일정 시간(여기서는 1초) 대기한 후에 이미지 파일 삭제
// 저장해놓은 이미지 파일의 URI에 대한 작업을 수행
viewUri?.let { uri ->
// 현재 쓰레드를 지정된 시간만큼 일시 중단 (1초 대기)
Thread.sleep(1000)
// 이미지 파일을 삭제
requireContext().contentResolver.delete(uri, null, null)
}
} catch (e: InterruptedException) {
// InterruptedException이 발생한 경우 예외 처리
e.printStackTrace()
}
// saveImageOnUnderAndroidQ: Android Q 미만에서 이미지를 저장하고 Uri를 반환하는 메서드
private fun saveImageOnUnderAndroidQ(bitmap: Bitmap): Uri? {
val fileName = System.currentTimeMillis().toString() + ".png"
val externalStorage = Environment.getExternalStorageDirectory().absolutePath
val path = "$externalStorage/DCIM/imageSave"
val dir = File(path)
if (dir.exists().not()) {
dir.mkdirs() // 폴더 없는 경우 폴더 생성
}
//저장할 파일 객체 생성
val fileItem = File("$dir/$fileName")
try {
fileItem.createNewFile() // 0kB 파일 생성
val fos = FileOutputStream(fileItem)
// 비트맵 압축
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.close()
// 미디어 스캔 실행하여 갤러리에 반영
MediaScannerConnection.scanFile(
requireContext().applicationContext,
arrayOf(fileItem.toString()),
null,
null
)
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
}
// FileProvider를 사용하여 content Uri 생성 및 반환
return FileProvider.getUriForFile(
requireContext().applicationContext,
"com.example.aboutme.fileprovider",
fileItem
)
}
//흰색 배경을 비트맵으로 나타내는 메서드
private fun drawBackgroundBitmap(): Bitmap {
// 기기 해상도를 가져옴.
val backgroundWidth = resources.displayMetrics.widthPixels
val backgroundHeight = resources.displayMetrics.heightPixels
val backgroundBitmap = Bitmap.createBitmap(backgroundWidth, backgroundHeight, Bitmap.Config.ARGB_8888) // 비트맵 생성
// 캔버스에 비트맵을 Mapping.
val canvas = Canvas(backgroundBitmap)
// 배경색을 흰색으로 지정
val bgColor = ContextCompat.getColor(requireContext(), android.R.color.white)
canvas.drawColor(bgColor) // 캔버스에 현재 설정된 배경화면색으로 칠한다.
return backgroundBitmap
}
// onRequestPermissionsResult: 권한 요청 결과를 처리하는 메서드
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// requestCode가 2인 경우 (외부 저장소 읽기 권한 요청)
when (requestCode) {
2 -> {
// 권한이 부여된 경우
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 권한이 부여된 경우에 수행할 동작
sharedViewModel.profileLayoutLiveData.value?.let { profileLayout ->
// 프로필 레이아웃을 Bitmap으로 변환
val viewBitmap = getViewBitmap(profileLayout)
// Android Q (Android 10) 미만에서 이미지를 저장하고 Uri를 반환하는 메서드 호출
val viewUri = saveImageOnUnderAndroidQ(viewBitmap)
val bgBitmap = drawBackgroundBitmap()
val bgUri = saveImageOnUnderAndroidQ(bgBitmap)
// Instagram 공유 실행
instaShare(bgUri,viewUri)
}
} else {
// 권한이 거부된 경우 토스트 메시지 표시
Toast.makeText(
requireContext(),
"외부 저장소 읽기 권한이 거부되었습니다.",
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
2.이미지 저장 메서드
외부 저장소에 저장해뒀다가 공유하겠다. -> Android Q / Android Q 미만의 로직을 나눈다. (Environment.getExternalStorageDirectory() 메서드가 Deprecated.)