일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 안드로이드 리스트뷰와 광고
- android 광고달기
- FlipView
- android 수익
- 앱에 광고 수익
- 우편물재난문자
- RecyclerView in Admob
- 정국라면
- 애드몹 설정
- 안드로이드
- Android
- 불그리레시피
- android kotlin
- android 앱업데이트 없이 변경하기
- 국제우편물
- 안드로이드 뒤집히는 뷰
- 정국라면레시피
- 라면레시피추천
- android 뒤집히는 카드뷰
- android remoteconfig
- Android AdMob
- 안드로이드 광고
- 앱에 광고달기
- 테러우편물
- 앱 광고 설정
- android 터치시 뒤집히는 뷰
- firebase RemoteConfig
- android 영단어 기능 만들기
- android notification
- kotlin
- Today
- Total
TAE
[Android/Kotlin] 비디오에서 썸네일 이미지 지정하기 - 비디오 썸네일 지정(2) 본문
앨범에서 영상을 선택하여 이미지를 보여주는 포스팅에 이어 해당 영상 초에 맞는 썸네일 이미지를 보여주는 기능을 포스팅하려 합니다.
앨범에서 영상 선택은 아래 포스팅 되어 있습니다.
https://tg-world.tistory.com/10
실행동작
위 영상과 같이 커스텀 앨범에서 영상을 선택하고 썸네일 선택하기를 누르면 하단에 seekbar로 해당 썸네일을 보여주며, 선택 클릭 시 선택했던 초에 맞는 썸네일 이미지를 보여주는 기능입니다. (인스타그램 썸네일 추출과 비슷한 기능입니다.)
코드
thumbanil 이라는 이름으로 packge를 만든 후 총 5개의 파일을 만들어 주었습니다.
CneterCropVideoView, SeekListener, ThumbnailTimeline, ThumbnailView, ThumbyActivity
위 소스는
https://github.com/bufferapp/Thumby
여기를 참고하여 변경 하여 작업하였습니다.
CneterCropVideoView.kt
class CenterCropVideoView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : TextureView(context, attrs), TextureView.SurfaceTextureListener {
private var mediaPlayer: MediaPlayer? = null
private var videoHeight = 0f
private var videoWidth = 0f
private var videoSizeDivisor = 1
private var mScalableType = ScalableType.CENTER_CROP
init {
surfaceTextureListener = this
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = false
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
mediaPlayer?.setSurface(Surface(surfaceTexture))
}
fun setDataSource(context: Context, uri: Uri, videoSizeDivisor: Int = 1) {
this.videoSizeDivisor = videoSizeDivisor
initPlayer()
mediaPlayer?.setDataSource(context, uri)
prepare()
}
fun seekTo(milliseconds: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mediaPlayer?.seekTo(milliseconds.toLong(), MediaPlayer.SEEK_CLOSEST)
}
}
fun getDuration(): Int {
return mediaPlayer?.duration ?: 0
}
private fun prepare() {
mediaPlayer?.setOnVideoSizeChangedListener { _, width, height ->
videoWidth = width.toFloat() / videoSizeDivisor
videoHeight = height.toFloat() / videoSizeDivisor
updateTextureViewSize()
seekTo(0)
}
mediaPlayer?.prepareAsync()
}
private fun updateTextureViewSize() {
val viewSize = Size(width, height)
val videoSize = Size(videoWidth.toInt(), videoHeight.toInt())
val scaleManager = ScaleManager(viewSize, videoSize)
val matrix = scaleManager.getScaleMatrix(mScalableType)
setTransform(matrix)
}
fun initPlayer() {
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer()
} else {
mediaPlayer?.reset()
}
}
}
썸네일 뷰 이미지를 표시할 커스텀 뷰입니다.
TexttureView를 상속받아 이미지를 그려줍니다.
TextTureView에 대한 자세한 사항은 공식 디벨롭 사이트를 확인하시면 됩니다.
https://source.android.com/docs/core/graphics/arch-tv?hl=ko
ThumbnailTimeline.kt
class ThumbnailTimeline @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : FrameLayout(context, attrs) {
private var frameDimensionW: Int = 0
private var frameDimensionH: Int = 0
var currentProgress = 0.0
var currentSeekPosition = 0f
var seekListener: SeekListener? = null
var uri: Uri? = null
set(value) {
field = value
field?.let {
loadThumbnails(it)
invalidate()
view_seek_bar.setDataSource(context, it, 4)
view_seek_bar.seekTo(currentSeekPosition.toInt())
}
}
init {
View.inflate(getContext(), R.layout.view_timeline, this)
frameDimensionW = context.resources.getDimensionPixelOffset(R.dimen.frames_video_width)
frameDimensionH = context.resources.getDimensionPixelOffset(R.dimen.frames_video_height)
isFocusable = true
isFocusableInTouchMode = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) elevation = 8f
val margin = 50
val params = RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
)
params.setMargins(margin, 0, margin, 0)
layoutParams = params
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event!!.action) {
MotionEvent.ACTION_MOVE -> handleTouchEvent(event)
}
return true
}
private fun handleTouchEvent(event: MotionEvent) {
val seekViewWidth = context.resources.getDimensionPixelSize(R.dimen.frames_video_width)
currentSeekPosition = (Math.round(event.x) - (seekViewWidth / 2)).toFloat()
val availableWidth = container_thumbnails.width -
(layoutParams as RelativeLayout.LayoutParams).marginEnd -
(layoutParams as RelativeLayout.LayoutParams).marginStart
if (currentSeekPosition + seekViewWidth > container_thumbnails.right) {
currentSeekPosition = (container_thumbnails.right - seekViewWidth).toFloat()
} else if (currentSeekPosition < container_thumbnails.left) {
currentSeekPosition = paddingStart.toFloat()
}
currentProgress = (currentSeekPosition.toDouble() / availableWidth.toDouble()) * 100
container_seek_bar.translationX = currentSeekPosition
view_seek_bar.seekTo(((currentProgress * view_seek_bar.getDuration()) / 100).toInt())
seekListener?.onVideoSeeked(currentProgress)
}
private fun loadThumbnails(uri: Uri) {
val metaDataSource = MediaMetadataRetriever()
metaDataSource.setDataSource(context, uri)
val videoLength = (metaDataSource.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION
)?.toInt()?.times(1000))?.toLong()
val thumbnailCount = 10
val interval = videoLength?.div(thumbnailCount)
for (i in 0 until thumbnailCount) {
val frameTime = i * interval!!
var bitmap =
metaDataSource.getFrameAtTime(frameTime, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
try {
var targetWidth: Int = 0
var targetHeight: Int = 0
if (bitmap != null) {
if (bitmap.height > bitmap.width) {
targetHeight = frameDimensionH
val percentage = frameDimensionH.toFloat() / bitmap.height
targetWidth = (bitmap.width * percentage).toInt()
} else {
targetWidth = frameDimensionW
val percentage = frameDimensionW.toFloat() / bitmap.width
targetHeight = (bitmap.height * percentage).toInt()
}
}
bitmap =
bitmap?.let { Bitmap.createScaledBitmap(it, targetWidth, targetHeight, false) }
} catch (e: Exception) {
e.printStackTrace()
}
container_thumbnails.addView(ThumbnailView(context).apply { setImageBitmap(bitmap) })
}
metaDataSource.release()
}
}
하단 타임라인 뷰를 그려주는 커스텀 뷰입니다.
ThumbnailView.kt
class ThumbnailView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatImageView(context, attrs) {
init {
scaleType = ScaleType.CENTER_CROP
setColorFilter(ContextCompat.getColor(context, R.color.video_dimmed))
val dimension = resources.getDimensionPixelSize(R.dimen.frames_video_width)
val dimension1 = resources.getDimensionPixelSize(R.dimen.frames_video_height)
layoutParams = LinearLayout.LayoutParams(dimension, dimension1).apply { weight = 1f }
}
}
ThumbyActivity.kt
class ThumbyActivity : ComponentActivity() {
companion object {
const val THUMBNAIL_POSITION = "thumbnail_position"
const val VIDEO_URI = "video_url"
const val VIDEO_THUMBNAIL_RESULT_OK = 1001
fun getStartIntent(context: Context, uri: Uri, thumbnailPosition: Long = 0): Intent {
val intent = Intent(context, ThumbyActivity::class.java)
intent.putExtra(VIDEO_URI, uri)
intent.putExtra(THUMBNAIL_POSITION, thumbnailPosition)
return intent
}
}
private lateinit var videoUri: Uri
private var location: Int = 0
private lateinit var binding: ActivityThumbyBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_thumby)
binding.uploadCV.setOnClickListener { finishWithData() }
binding.closeIV.setOnClickListener { finish() }
videoUri = intent.getParcelableExtra(VIDEO_URI)!!
setupVideoContent(videoUri)
}
private fun setupVideoContent(videoUri: Uri) {
binding.thumbnailCCV.setDataSource(this, videoUri)
binding.thumbsTTL.seekListener = seekListener
binding.thumbsTTL.currentSeekPosition = intent.getLongExtra(THUMBNAIL_POSITION, 0).toFloat()
binding.thumbsTTL.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.thumbsTTL.viewTreeObserver.removeOnGlobalLayoutListener(this)
binding.thumbsTTL.uri = videoUri
}
})
}
private fun finishWithData() {
val intent = Intent()
intent.putExtra(
THUMBNAIL_POSITION,
location
)
intent.putExtra(VIDEO_URI, videoUri)
setResult(VIDEO_THUMBNAIL_RESULT_OK, intent)
finish()
}
private val seekListener = object : SeekListener {
override fun onVideoSeeked(percentage: Double) {
location = (percentage.toInt() * binding.thumbnailCCV.getDuration()) / 100
binding.thumbnailCCV.seekTo((percentage.toInt() * binding.thumbnailCCV.getDuration()) / 100)
}
}
}
썸네일 지정하는 Activity입니다.
filepath를 url로 사용하여 해당 파일을 가지고 와 미디어 파일에 연결 후 해당 초에 맞는 이미지를 랜더링 해줍니다.
MainActivity.kt
private var filePath = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.thumbnailCCV.initPlayer()
커스텀뷰를 init 시켜줍니다
binding.selectThumbnailBT.setOnClickListener {
activityForResult.launch(
ThumbyActivity.getStartIntent(
this@MainActivity,
Uri.parse(filePath),
0
)
)
}
썸네일 선택하기 버튼을 클릭할 경우
ActivityResultLauncher으로 ThumbyActivity를 호출하여 줍니다.
이때 파라미터는 context와 파일 url을 넘겨주시면 됩니다.
결괏값
private fun activityForResult(){
activityForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
if (it.resultCode == RESULT_OK){
binding.selectThumbnailBT.visibility = View.VISIBLE
filePath = it.data?.getStringExtra("videoData")!!
binding.thumbnailCCV.setDataSource(this, Uri.parse(filePath), 1)
} else if (it.resultCode == VIDEO_THUMBNAIL_RESULT_OK){
val location: Int = it.data?.getIntExtra(THUMBNAIL_POSITION, 0)!!
binding.thumbnailCCV.seekTo(location)
}
}
}
cutsomAlbumActivity에서 setResult으로 받은 데이터인 파일 url로 커스텀 뷰에 setData를 해줍니다.
ThumbyActivity에서 setResult으로 받은 데이터인 초 값으로 seekTo를 설정하여 줍니다.
layout
activity_thumby.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<!--타이틀-->
<RelativeLayout
android:id="@+id/headerRL"
android:layout_width="match_parent"
android:layout_height="48dp">
<ImageView
android:id="@+id/closeIV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_baseline_arrow_back_24" />
<androidx.cardview.widget.CardView
android:id="@+id/uploadCV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="6dp"
app:cardBackgroundColor="#BABABA"
app:cardCornerRadius="4dp"
app:cardElevation="0dp"
app:contentPaddingBottom="6dp"
app:contentPaddingLeft="12dp"
app:contentPaddingRight="12dp"
app:contentPaddingTop="6dp">
<TextView
android:id="@+id/btn_upload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="선택"
android:textColor="#FFFF"
android:textSize="14sp"
android:textStyle="normal" />
</androidx.cardview.widget.CardView>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/thumbsTTL"
android:layout_below="@+id/headerRL"
android:orientation="vertical"
android:gravity="center">
<androidx.cardview.widget.CardView
android:id="@+id/btn_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp"
app:cardElevation="0dp">
<com.example.selectvideothumbnail.thumbnail.CenterCropVideoView
android:id="@+id/thumbnailCCV"
android:layout_width="@dimen/frames_thumbnail_width"
android:layout_height="@dimen/frames_thumbnail_height"
android:layout_gravity="center" />
</androidx.cardview.widget.CardView>
<ImageView
android:id="@+id/test"
android:layout_width="40dp"
android:layout_height="40dp"
/>
</LinearLayout>
<com.example.selectvideothumbnail.thumbnail.ThumbnailTimeline
android:id="@+id/thumbsTTL"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="28dp" />
</RelativeLayout>
</layout>
썸네일 선택하는 ThumbyActivity의 레이아웃입니다.
썸네일 선택하는 화면 커스텀하여 사용 가능합니다.
view_tiemline.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent">
<LinearLayout
android:id="@+id/container_thumbnails"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal" />
<FrameLayout
android:id="@+id/container_seek_bar"
android:layout_width="wrap_content"
android:layout_height="@dimen/frames_timeline_height"
android:layout_gravity="center_vertical">
<com.google.android.material.card.MaterialCardView
android:id="@+id/thumbsMCV"
android:layout_width="wrap_content"
android:layout_height="@dimen/frames_timeline_height"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="28dp"
android:theme="@style/Theme.MaterialComponents.Light"
app:cardCornerRadius="3dp"
app:rippleColor="@android:color/transparent"
app:strokeColor="#FD2678"
app:strokeWidth="4dp">
<com.example.selectvideothumbnail.thumbnail.CenterCropVideoView
android:id="@+id/view_seek_bar"
android:layout_width="@dimen/frames_timeline_width"
android:layout_height="@dimen/frames_timeline_height" />
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
</merge>
썸네일 선택 시 하단 seekbar 커스텀하는 view입니다.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.selectvideothumbnail.thumbnail.CenterCropVideoView
android:id="@+id/thumbnailCCV"
android:layout_width="200dp"
android:layout_height="300dp"
app:layout_constraintBottom_toTopOf="@id/goAlbumTV"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/goAlbumTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="앨범 불러오기"
app:layout_constraintBottom_toTopOf="@id/selectThumbnailBT"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<Button
android:id="@+id/selectThumbnailBT"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="썸네일 선택하기"
android:visibility="gone"
app:layout_constraintVertical_bias="0.8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
앨범 불러오기, 썸네일 선택하기 버튼 및 썸네일 이미지가 표시되는 mainActivity의 레이아웃입니다.
이상 비디오에서 썸네일 추출하기 포스팅을 마치겠습니다.
궁금 사항이나 풀 소스를 원하시면 따로 요청 주시면 답변드리겠습니다.
감사합니다.
'android > 코드' 카테고리의 다른 글
[android/Kotlin] 푸시 메시지 구현 - 푸시 메시지 구현(이미지 푸시, 긴글 푸시) (0) | 2023.03.22 |
---|---|
[android/Kotlin] 푸시 메시지 구현 - firebase 프로젝트 만들기 (0) | 2023.03.21 |
[Android/Kotlin] registerForActivityResult사용하여 앨범에서 사진 선택하기- 비디오 썸네일 지정(1) (0) | 2023.03.20 |
[Android/Kotlin] 커스텀 앨범 만들기 - 커스텀 갤러리(2) (0) | 2023.03.17 |
[Android/Kotlin] 갤러리 접근 권한(Premission) 설정하기 - 커스텀 갤러리(1) (0) | 2023.03.16 |