일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 notification
- 불그리레시피
- android 앱업데이트 없이 변경하기
- 안드로이드 뒤집히는 뷰
- FlipView
- android 터치시 뒤집히는 뷰
- android remoteconfig
- 라면레시피추천
- RecyclerView in Admob
- android 뒤집히는 카드뷰
- 안드로이드
- 안드로이드 광고
- kotlin
- 앱 광고 설정
- Android AdMob
- android 수익
- 테러우편물
- Android
- 정국라면레시피
- 정국라면
- android 영단어 기능 만들기
- 우편물재난문자
- firebase RemoteConfig
- android kotlin
- android 광고달기
- 앱에 광고 수익
- 애드몹 설정
- Today
- Total
TAE
[Android/Kotlin] 숏츠 화면 만들기 (ExoPlayer) 본문
이번 포스팅에는 유튜브나 인스타 처럼 넘기는 영상 숏츠 화면을 만들어 보겠습니다.
위 처럼 짧은 영상을 넘기면서 볼 수 있는 숏츠 화면을 만들어 보겠습니다.
ViewPager2 + ExoPlayer 사용하여 만들어 보겠습니다.
Fragment에 ExoPlayer를 구성하여 만들었으며, Viewpager Adapger에 createFragment로 각 포지션마다 아이템을 넘겨 fragment를 구성하여 각 화면을 보여주게 됩니다.
그럼 소스 보며 하나씩 설명 드리겠습니다.
1. build.gradle (app)
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.android.exoplayer:exoplayer:2.15.1'
viewpager와 exoplayer를 build.gradle app단에 추가하여 줍니다.
2. activity_short.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/shortsVP"
android:saveEnabled="false"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</LinearLayout>
</layout>
short를 만들 xml에 viewpager2를 추가하여 줍니다.
orientaiton을 vertical로 설정하게 되면 유튜브처럼 세로로 넘길수 있고 horizental로 설정하고 애니메이션을 주게 되면 인스타 스토리처럼 가로로 넘길수 있습니다.
3. ShortsActivity.kt
class ShortsActivity : AppCompatActivity() {
private lateinit var binding: ActivityShortBinding
private lateinit var videoAdapter: VideoPlayerAdapter
private val mediaObjectList = ArrayList<ShortObject>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_short)
val ShortObject1 = ShortObject()
ShortObject1.setId(1)
ShortObject1.setUserName("고양이 집사")
ShortObject1.setContents("냥냥펀치 입니다~~")
ShortObject1.setUrl("https://blog.kakaocdn.net/dn/cPjc1G/btslYxcRuz4/AqGJ0ifzNuvIY3ywsHMDv0/KakaoTalk_Video_2023-06-30-17-46-03.mp4?attach=1&knm=tfile.mp4")
val ShortObject2 = ShortObject()
ShortObject2.setId(2)
ShortObject2.setUserName("하잉하잉")
ShortObject2.setContents("고양이 영상입니다~ 집앞 고양이~")
ShortObject2.setUrl("https://blog.kakaocdn.net/dn/bcVeEV/btslZPqcb1L/6d2xMDsAqOaqRK5eeoeQxK/KakaoTalk_Video_2023-06-30-17-47-22.mp4?attach=1&knm=tfile.mp4")
val ShortObject3 = ShortObject()
ShortObject3.setId(3)
ShortObject3.setUserName("아기고양이")
ShortObject3.setContents("아기 고양이 입니다!! 야옹")
ShortObject3.setUrl("https://blog.kakaocdn.net/dn/riYGP/btslT0e0Zba/eIhERthBv1EmexAHCZF4PK/KakaoTalk_Video_2023-06-30-14-57-27.mp4?attach=1&knm=tfile.mp4")
mediaObjectList.add(ShortObject1)
mediaObjectList.add(ShortObject2)
mediaObjectList.add(ShortObject3)
videoAdapter = VideoPlayerAdapter(this, mediaObjectList)
binding.shortsVP.adapter = videoAdapter
binding.shortsVP.currentItem = 0
}
}
VideoPlayerAdapter에 임시로 만든 ShortObject들을 넘겨줍니다.
ShortObject는 임시로 userName, Contents , mediaUrl을 담고있습니다.
4. ShortObject.kt
class ShortObject : Serializable {
private var uId = 0
private var contents: String? = null
private var mediaUrl: String? = null
private var userName: String? = null
fun getId(): Int {
return uId
}
fun setId(uId: Int) {
this.uId = uId
}
fun getContents(): String? {
return contents
}
fun setContents(mTitle: String?) {
contents = mTitle
}
fun getUrl(): String? {
return mediaUrl
}
fun setUrl(mUrl: String?) {
mediaUrl = mUrl
}
fun getUserName(): String? {
return userName
}
fun setUserName(mCoverUrl: String?) {
userName = mCoverUrl
}
}
숏츠 오브젝트의 정보를 담을 모델입니다
4. VideoPlayerAdapter
class VideoPlayerAdapter(fragmentActivity: FragmentActivity, arr: ArrayList<ShortObject>) :
FragmentStateAdapter(fragmentActivity) {
private var videoDetailList: ArrayList<ShortObject>? = arr
override fun getItemCount(): Int {
return videoDetailList!!.size
}
override fun getItemId(position: Int): Long {
return super.getItemId(position)
}
override fun containsItem(itemId: Long): Boolean {
return super.containsItem(itemId)
}
override fun createFragment(position: Int): Fragment {
return VideoPlayerFragment.newInstance(
videoDetailList?.get(position)
)
}
}
Activity에서 받은 ShortObject ArrayList를 받아 createFragment에서 newInstace로 각 포지션에 맞는 리스트 데이터들을 넘겨줍니다.
찾아보니 newInstance 대신 koin으로 사용하라는 이야기도 있었는데.. 우선 구현에 목적성을 두어 newInstance를 사용하겠습니다.
고도화 하시고 싶으시면 한번 찾아 보시는 걸 추천 드립니다.
5. fragment_video_player.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--플레이어뷰-->
<com.google.android.exoplayer2.ui.PlayerView
android:visibility="visible"
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
app:ad_marker_color="@color/black"
app:buffered_color="@color/black"
app:controller_layout_id="@layout/layout_video_controller"
app:hide_on_touch="false"
app:keep_content_on_player_reset="false"
app:played_ad_marker_color="@color/black"
app:played_color="@color/black"
app:resize_mode="fit"
app:scrubber_color="@color/black"
app:show_timeout="0"
app:shutter_background_color="@color/black"
app:unplayed_color="@color/black" />
<!--로딩 프로그래스바-->
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</layout>
playView 기본 속성으로는 아래를 참고하시면 됩니다.
- bar_height- 타임 바의 높이 치수.
- touch_target_height- 시간 표시줄과의 터치 상호 작용이 처리되는 영역의 높이에 대한 치수입니다. 높이를 지정하지 않으면 보기의 높이도 결정됩니다.
- ad_marker_width- 표시줄에 표시되는 광고 마커의 너비 치수입니다. 광고 마커는 광고가 재생되는 시간을 표시하기 위해 시간 표시줄에 중첩됩니다.
- scrubber_enabled_size- 스크러빙이 활성화되었지만 진행 중이 아닐 때 원형 스크러버 핸들의 직경 치수. 스크러버 핸들이 표시되지 않으면 0으로 설정하십시오.
- scrubber_disabled_size- 스크러빙이 활성화되지 않은 경우 원형 스크러버 핸들의 직경 치수. 스크러버 핸들이 표시되지 않으면 0으로 설정하십시오.
- scrubber_dragged_size- 스크러빙이 진행 중일 때 원형 스크러버 핸들의 직경에 대한 치수입니다. 스크러버 핸들이 표시되지 않으면 0으로 설정하십시오.
- scrubber_drawable- 스크러버 핸들에 대해 그릴 드로어블에 대한 선택적 참조입니다. 설정하면 스크러버 핸들의 원을 그리는 기본 동작을 재정의합니다.
- played_color- 현재 재생 위치 이전의 미디어를 나타내는 시간 표시줄 부분의 색상입니다.
- scrubber_color- 스크러버 핸들의 색상.
- buffered_color- 현재 재생 위치 이후부터 현재 버퍼링된 위치까지의 시간 표시줄 부분에 대한 색상입니다.
- unplayed_color- 현재 버퍼링된 위치 이후의 시간 표시줄 부분에 대한 색상입니다.
- ad_marker_color- 재생되지 않은 광고 마커의 색상.
- played_ad_marker_color- 재생된 광고 마커의 색상.
6. VideoPlayerFramgent.kt
class VideoPlayerFragment : Fragment() {
private lateinit var binding: FragmentVideoPlayerBinding
private var player: SimpleExoPlayer? = null
private var mIsVisibleToUser = false
private var chkPlay: Boolean = true
private lateinit var shortObject : ShortObject
companion object {
fun newInstance(mediaUrl: ShortObject?): VideoPlayerFragment {
val args = Bundle().apply {
putSerializable("mediaUrl", mediaUrl)
}
val fragment = VideoPlayerFragment()
fragment.arguments = args
return fragment
}
}
override fun setMenuVisibility(menuVisible: Boolean) {
super.setMenuVisibility(menuVisible)
if (menuVisible) {
mIsVisibleToUser = menuVisible
playerControl(play = true, reset = false)
} else {
playerControl(play = false, reset = true)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
shortObject = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireArguments().getSerializable("mediaUrl", ShortObject::class.java)!!
} else {
(requireArguments().getSerializable("mediaUrl") as? ShortObject)!!
}
} catch (e: java.lang.Exception) {
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_video_player, container, false)
initializePlayer()
binding.playerView.setOnClickListener {
playerControl(chkPlay, false)
}
setContents()
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
releasePlayer()
}
override fun onDestroy() {
super.onDestroy()
releasePlayer()
}
private fun releasePlayer() {
if (player != null) {
player!!.release()
player = null
}
}
private fun setContents(){
binding.playerView.findViewById<TextView>(R.id.tv_nickname).text = shortObject.getUserName()
binding.playerView.findViewById<TextView>(R.id.tv_text_short).text = shortObject.getContents()
}
private fun initializePlayer() {
try {
if (player == null) {
val loadControl = DefaultLoadControl.Builder()
.setAllocator(DefaultAllocator(true, 16))
.setBufferDurationsMs(2000, 3000, 1500, 2000)
.setTargetBufferBytes(-1)
.setPrioritizeTimeOverSizeThresholds(true)
.createDefaultLoadControl()
player =
SimpleExoPlayer.Builder(requireContext()) // .setTrackSelector(trackSelector)
.setLoadControl(loadControl) // .setBandwidthMeter(defaultBandwidthMeter)
.build()
// 음소거 사용자 설정 유지
binding.playerView.player = player
binding.playerView.useController = true
binding.playerView.showController()
val mediaSource: MediaSource = buildMediaSource(Uri.parse(shortObject.getUrl()))!!
if (player != null) {
player!!.prepare(mediaSource, true, false)
player!!.addListener(eventListener)
}
}
} catch (e: Exception) {
}
}
fun buildMediaSource(uri: Uri): MediaSource? {
val userAgent = Util.getUserAgent(requireContext(), resources.getString(R.string.app_name))
val defaultHttpDataSourceFactory = DefaultHttpDataSourceFactory(userAgent)
val defaultDataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
requireContext(), userAgent
)
val lastPathSegment = uri.lastPathSegment
return if (lastPathSegment!!.contains("mp3") || lastPathSegment.contains("mp4")) {
ProgressiveMediaSource.Factory(defaultHttpDataSourceFactory)
.createMediaSource(uri)
} else if (lastPathSegment.contains("m3u8")) {
HlsMediaSource.Factory(defaultHttpDataSourceFactory)
.setAllowChunklessPreparation(true)
.createMediaSource(uri)
} else if (lastPathSegment.contains("mpd")) {
DashMediaSource.Factory(defaultDataSourceFactory)
.createMediaSource(uri)
} else {
ProgressiveMediaSource.Factory(defaultDataSourceFactory)
.createMediaSource(uri)
}
}
private var eventListener: Player.EventListener = object : Player.EventListener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
chkPlay = isPlaying
}
/**
* @param playWhenReady - Whether playback will proceed when ready.
* @param playbackState - One of the STATE constants.
*/
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
when (playbackState) {
SimpleExoPlayer.STATE_IDLE -> {
}
SimpleExoPlayer.STATE_BUFFERING -> {
}
SimpleExoPlayer.STATE_READY -> {
}
SimpleExoPlayer.STATE_ENDED -> {
playerControl(true, true)
}
else -> {
}
}
}
}
fun playerControl(play: Boolean, reset: Boolean) {
try {
if (play) { // start
if (mIsVisibleToUser) {
if (player != null && !player!!.isPlaying) {
if (reset) {
player!!.seekTo(0)
}
player!!.playWhenReady = true
}
if (binding.playerView != null) {
binding.playerView.onResume()
}
}
} else { // stop
if (binding.playerView != null) {
binding.playerView.onPause()
}
if (player != null) {
player!!.playWhenReady = false
if (reset) {
player!!.seekTo(0)
}
}
}
} catch (e: java.lang.Exception) {
}
}
}
코드 설명
bundle 로 ShortObject를 받아온 후 화면에 그려줍니다.
override fun setMenuVisibility(menuVisible: Boolean) {
super.setMenuVisibility(menuVisible)
if (menuVisible) {
mIsVisibleToUser = menuVisible
playerControl(play = true, reset = false)
} else {
playerControl(play = false, reset = true)
}
}
setMenuVisibility는 현재 화면을 보고 있는지 여부를 넘겨줍니다.
menuVisible이 ture이면 playerControl 함수에서 영상 재생을 flase일 경우 영상 정지후 reset을 시켜줍니다.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding =
DataBindingUtil.inflate(inflater, R.layout.fragment_video_player, container, false)
initializePlayer()
binding.playerView.setOnClickListener {
playerControl(chkPlay, false)
}
setContents()
return binding.root
}
onCreateView에서 player를 init하는 함수를 실행하고 player클릭시 재생 멈춤 클릭 리스너를 등록하고, 해당 컨텐츠 내용을 init하여 줍니다.
참조
https://exoplayer.dev/doc/reference/constant-values.html
https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/DefaultTimeBar.html
'android' 카테고리의 다른 글
[Android/Kotlin] RemoteConfig로 클릭하면 뒤집히는 뷰로 영단어 암기장 만들기 (0) | 2023.08.10 |
---|---|
[Android/Kotlin] 클릭하면 뒤집히는 뷰로 영단어 암기장 만들기 flip view(플립뷰) (0) | 2023.08.10 |
[Android/Kotlin]SNS(google) 로그인 (0) | 2023.06.25 |
[Android] 안드로이드 스튜디오 글씨체 폰트파일 적용 (0) | 2023.06.23 |
[android studio] 로그캣 예전 버전으로 설정하기 (0) | 2023.06.22 |