首页 安卓& Kotlin Books 安卓Apprentice

27
剧集玩家 由汤姆加金哲学撰写

在最后一章,你成功地添加音频播放的应用程序,但你戛然而止添加任何内置的播放功能。在本节的最后一章,你会通过添加视频完整播放界面和支持,完成了PodPlay。

如果你有自己的项目下面一起,本章的启动项目包括一个附加的图标,你需要完成的部分。打开项目,然后将下面的资源从所提供的启动项目为此致复制。一定要复制 .png. 来自各种文件 DPI. 文件夹(如下所示一旦显示为“?DPI”,但在文件系统上,它们将是“HDPI”,“MDPI”等)。这包括以下资源:

  • RES / drawable-?DPI / ic_forward_30_white.png
  • RES / drawable-?DPI / ic_replay_10_white.png
  • Res / drawable / ic_play_pause_toggle.xml

如果你没有“自己的项目,不要担心。找到 项目 本章的文件夹并打开 Podplay. 项目内部 起动机 folder.

您第一次打开项目时,Android Studio需要几分钟时间来设置环境并更新其依赖项。

开奖结果3d

您可以通过添加一个新片段显示单个事件细节入手。这个片段被载入当用户点击的插曲。

Episode Detail屏幕提供了剧集和播放控件的概述。设计如下所示:

The album art is in the upper-left corner. The episode title is to the right. The description takes up the entire center of the layout; and because episode descriptions can be long, the TextView is scrollable so that the user can see the full description.

底部是播放器控制区域。该区域具有黑色背景和以下控件:

  • 播放/暂停切换:开始并停止播放。
  • 跳过:跳回10秒钟。
  • 向前跳过:跳过30秒。
  • 速度控制:允许重放速度被增加。
  • 洗涤器:释放播放进度和依赖于集发作的部分的播放进度。

首先,创建基本布局。

剧集播放器布局

里面 res /布局,创建一个新文件并命名它 fragment_episode_player.xml.。用以下内容替换其内容:

<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"
  android:background="@android:color/black"
  tools:context="com.raywenderlich.podplay.ui.epiSodePlayerFragment.">

  <SurfaceView
      android:id="@+id/videoSurfaceView"
      android:layout_width="0dp"
      android:layout_height="0dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintBottom_toBottomOf="parent"
      android:visibility="invisible"/>

  <androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/headerView"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:background="#eeeeee"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

  </androidx.constraintlayout.widget.ConstraintLayout>

  <TextView
      android:id="@+id/episodeDescTextView"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:background="@android:color/white"
      android:padding="8dp"
      android:scrollbars="vertical"
      app:layout_constraintBottom_toTopOf="@+id/playerControls"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/headerView"
      tools:text="Episode description"/>

  <androidx.constraintlayout.widget.ConstraintLayout
      android:id="@+id/playerControls"
      android:layout_width="0dp"
      android:layout_height="76dp"
      android:background="@android:color/background_dark"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintBottom_toBottomOf="parent">
  
  </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
  android:id="@+id/episodeImageView"
  android:layout_width="60dp"
  android:layout_height="60dp"
  android:layout_marginStart="8dp"
  android:layout_marginTop="8dp"
  android:src="@android:drawable/ic_menu_report_image"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent"/>

<TextView
    android:id="@+id/episodeTitleTextView"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_marginEnd="8dp"
    android:layout_marginStart="8dp"
    android:text=""
    app:layout_constraintBottom_toBottomOf="@+id/episodeImageView"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toEndOf="@+id/episodeImageView"
    app:layout_constraintTop_toTopOf="@+id/episodeImageView"
    tools:text="Episode Title"/>
<ImageButton
    android:id="@+id/replayButton"
    android:layout_width="34dp"
    android:layout_height="34dp"
    android:layout_marginEnd="24dp"
    android:layout_marginTop="8dp"
    android:background="@android:color/transparent"
    android:scaleType="fitCenter"
    android:src="@drawable/ic_replay_10_white"
    app:layout_constraintEnd_toStartOf="@+id/playToggleButton"
    app:layout_constraintTop_toTopOf="parent"/>

<Button
    android:id="@+id/playToggleButton"
    android:layout_width="34dp"
    android:layout_height="34dp"
    android:layout_marginTop="8dp"
    android:background="@drawable/ic_play_pause_toggle"
    android:scaleType="fitCenter"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

<ImageButton
    android:id="@+id/forwardButton"
    android:layout_width="34dp"
    android:layout_height="34dp"
    android:layout_marginStart="24dp"
    android:layout_marginTop="8dp"
    android:background="@android:color/transparent"
    android:scaleType="fitCenter"
    android:src="@drawable/ic_forward_30_white"
    app:layout_constraintStart_toEndOf="@+id/playToggleButton"
    app:layout_constraintTop_toTopOf="parent"/>

<Button
    android:id="@+id/speedButton"
    android:layout_width="54dp"
    android:layout_height="34dp"
    android:layout_marginEnd="8dp"
    android:layout_marginTop="8dp"
    android:background="@android:color/transparent"
    android:text="1x"
    android:textColor="@android:color/white"
    android:textSize="14sp"
    android:textAllCaps="false"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>    
<TextView
    android:id="@+id/currentTimeTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:layout_marginStart="8dp"
    android:text="0:00"
    android:textColor="@android:color/white"
    android:textSize="12sp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@+id/seekBar"/>

<SeekBar
    android:id="@+id/seekBar"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:layout_marginEnd="8dp"
    android:layout_marginStart="8dp"
    android:progressBackgroundTint="@android:color/white"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toStartOf="@+id/endTimeTextView"
    app:layout_constraintStart_toEndOf="@+id/currentTimeTextView"/>

<TextView
    android:id="@+id/endTimeTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:layout_marginEnd="8dp"
    android:text="0:00"
    android:textColor="@android:color/white"
    android:textSize="12sp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="@+id/seekBar"/>

剧集球员片段

You’re ready to build out the episode player Fragment. This Fragment will display the episode layout and handle all of the playback logic. You’ll move the media related code from the PodcastDetailsFragment class into this new episode player fragment.

class epiSodePlayerFragment. : Fragment() {

  companion object {
    fun newInstance(): epiSodePlayerFragment. {
      return epiSodePlayerFragment.()
    }
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    retainInstance = true
  }

  override fun onCreateView(inflater: LayoutInflater,
                            container: ViewGroup?,
                            savedInstanceState: Bundle?): View?{
    return inflater.inflate(R.layout.fragment_episode_player,
        container, false)
  }

  override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
  }

  override fun onStart() {
    super.onStart()
  }

  override fun onStop() {
    super.onStop()
  }
}

剧集播放器导航

在精制完成摩擦码导航之前。

fun onShowEpisodePlayer(episodeViewData: EpisodeViewData)
listener?.onShowEpisodePlayer(episodeViewData)
override fun onShowEpisodePlayer(episodeViewData: EpisodeViewData) {
}
private const val TAG_PLAYER_FRAGMENT = "PlayerFragment"
private fun createepiSodePlayerFragment.(): epiSodePlayerFragment. {
  var episodePlayerFragment =
      supportFragmentManager.findFragmentByTag(TAG_PLAYER_FRAGMENT) as
      epiSodePlayerFragment.?

  if (episodePlayerFragment == null) {
    episodePlayerFragment = epiSodePlayerFragment..newInstance()
  }

  return episodePlayerFragment
}
var activeEpisodeViewData: EpisodeViewData? = null
private fun showPlayerFragment() {
  val episodePlayerFragment = createepiSodePlayerFragment.()

  supportFragmentManager.beginTransaction().replace(
      R.id.podcastDetailsContainer,
      episodePlayerFragment,
      TAG_PLAYER_FRAGMENT
    ).addToBackStack("PlayerFragment").commit()
  podcastRecyclerView.visibility = View.INVISIBLE
  searchMenuItem.isVisible = false
}
podcastViewModel.activeEpisodeViewData = episodeViewData
showPlayerFragment()

剧集玩家细节

现在是时候从播放器屏幕得到一些插曲数据。您将使用从播客视图模型活动视图插曲数据来填充视图。

private val podcastViewModel: PodcastViewModel by activityViewModels()
private fun updateControls() {
  // 1
  episodeTitleTextView.text =
      podcastViewModel.activeEpisodeViewData?.title

  // 2
  val htmlDesc =
      podcastViewModel.activeEpisodeViewData?.description ?: ""
  val descSpan = HtmlUtils.htmlToSpannable(htmlDesc)
  episodeDescTextView.text = descSpan
  episodeDescTextView.movementMethod = ScrollingMovementMethod()

  // 3
  val fragmentActivity = activity as FragmentActivity
  Glide.with(fragmentActivity)
    .load(podcastViewModel.activePodcastViewData?.imageUrl)
    .into(episodeImageView)
}
updateControls()

剧集播放器控制

现在,您可以将注意力转向播放器控件。首先,您将获得基本播放,暂停和跳过控制;然后你将专注于寻求酒吧和速度控制。

播放/暂停按钮

现在是时候挂钩游戏/ LAGON按钮开始和停止播放。

private fun togglePlayPause() {
  val fragmentActivity = activity as FragmentActivity
  val controller = MediaControllerCompat.getMediaController(fragmentActivity)
  if (controller.playbackState != null) {
    if (controller.playbackState.state ==
        PlaybackStateCompat.STATE_PLAYING) {
      controller.transportControls.pause()
    } else {
      podcastViewModel.activeEpisodeViewData?.let { startPlaying(it) }
    }
  } else {
    podcastViewModel.activeEpisodeViewData?.let { startPlaying(it) }
  }
}
private fun setupControls() {
  playToggleButton.setOnClickListener {
    togglePlayPause()
  }
}
private fun handleStateChange(state: Int) {
  val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
  playToggleButton.isActivated = isPlaying
}
val state = state ?: return
handleStateChange(state.getState())
setupControls()

速度控制按钮

接下来,您将挂钩速度控制按钮。此按钮将使速度提高0.25倍,每次删除最大值为2.0倍。达到最大值2.0倍后,它将转至0.75倍。

companion object {
  const val CMD_CHANGESPEED = "change_speed"
  const val CMD_EXTRA_SPEED = "speed"
}
private fun setState(state: Int, newSpeed: Float? = null) {
// 1
var speed = 1.0f
// 2
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  if (newSpeed == null) {
    // 3
    speed = mediaPlayer?.getPlaybackParams()?.speed ?: 1.0f
  } else {
    // 4
    speed = newSpeed
  }
  mediaPlayer?.let { mediaPlayer ->
    // 5
    try {
      mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speed)
    }
    catch (e: Exception) {
      // 6
      mediaPlayer.reset()
      mediaUri?.let { mediaUri ->      
        mediaPlayer.setDataSource(context, mediaUri)
      }
      mediaPlayer.prepare()
      // 7
      mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speed)
      // 8
      mediaPlayer.seekTo(position.toInt())
      // 9
      if (state == PlaybackStateCompat.STATE_PLAYING) {
        mediaPlayer.start()
      }
    }
  }
}
.setState(state, position, speed)
private fun changeSpeed(extras: Bundle) {
  var playbackState = PlaybackStateCompat.STATE_PAUSED
  if (mediaSession.controller.playbackState != null) {
    playbackState = mediaSession.controller.playbackState.state
  }
  setState(playbackState, extras.getFloat(CMD_EXTRA_SPEED))
}
override fun onCommand(command: String?, extras: Bundle?,
    cb: ResultReceiver?) {
  super.onCommand(command, extras, cb)
  when (command) {
    CMD_CHANGESPEED -> extras?.let { changeSpeed(it) }
  }
}
private var playerSpeed: Float = 1.0f
private fun changeSpeed() {
  // 1
  playerSpeed += 0.25f
  if (playerSpeed > 2.0f) {
    playerSpeed = 0.75f
  }
  // 2
  val bundle = Bundle()
  bundle.putFloat(CMD_EXTRA_SPEED, playerSpeed)
  // 3    
  val fragmentActivity = activity as FragmentActivity
  val controller = MediaControllerCompat.getMediaController(fragmentActivity)
  controller.sendCommand(CMD_CHANGESPEED, bundle, null)
  // 4
  speedButton.text = "${playerSpeed}x"
}
speedButton.text = "${playerSpeed}x"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  speedButton.setOnClickListener {
    changeSpeed()
  }
} else {
  speedButton.visibility = View.INVISIBLE
}

寻求

Before adding the changes to the player Fragment to support skipping or scrubbing to a new position, you need to update the media browser to allow seeking to a specific playback position. This is done by overriding an additional method in PodplayMediaCallback.

override fun onSeekTo(pos: Long) {
  super.onSeekTo(pos)
  // 1
  mediaPlayer?.seekTo(pos.toInt())
  // 2
  val playbackState: PlaybackStateCompat? =
      mediaSession.controller.playbackState
  // 3
  if (playbackState != null) {
    setState(playbackState.state)
  } else {
    setState(PlaybackStateCompat.STATE_PAUSED)
  }
}
private fun seekBy(seconds: Int) {
  val fragmentActivity = activity as FragmentActivity
  val controller = MediaControllerCompat.getMediaController(fragmentActivity)
  val newPosition = controller.playbackState.position + seconds*1000
  controller.transportControls.seekTo(newPosition)
}

跳过按钮

OK,现在是时候公开从事的前后跳跃功能。媒体控制器允许你直接更改播放位置。执行跳跃,你需要采取当前播放位置,加正负抵消得到一个新的位置,然后设置新的位置。

forwardButton.setOnClickListener {
  seekBy(30)
}
replayButton.setOnClickListener {
  seekBy(-10)
}

洗涤器控制

有几个步骤需要制作划线功能:

.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
    mediaPlayer.duration.toLong())
private var episodeDuration: Long = 0
private fun updateControlsFromMetadata(metadata: MediaMetadataCompat) {
  episodeDuration =
      metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
  endTimeTextView.text = DateUtils.formatElapsedTime(
      episodeDuration / 1000)
}
我tadata?.let { updateControlsFromMetadata(it) }
seekBar.max = episodeDuration.toInt()
private var draggingScrubber: Boolean = false
// 1
seekBar.setOnSeekBarChangeListener(
    object : SeekBar.OnSeekBarChangeListener {
  override fun onProgressChanged(seekBar: SeekBar, progress: Int,
      fromUser: Boolean) {
    // 2
    currentTimeTextView.text = DateUtils.formatElapsedTime(
        (progress / 1000).toLong())
  }
  override fun onStartTrackingTouch(seekBar: SeekBar) {
    // 3
    draggingScrubber = true
  }
  override fun onStopTrackingTouch(seekBar: SeekBar) {
    // 4
    draggingScrubber = false
    // 5    
    val fragmentActivity = activity as FragmentActivity
    val controller = MediaControllerCompat.getMediaController(fragmentActivity)
    if (controller.playbackState != null) {
      // 6
      controller.transportControls.seekTo(seekBar.progress.toLong())
    } else {
      // 7
      seekBar.progress = 0
    }
  }
})
private var progressAnimator: ValueAnimator? = null
// 1
private fun animateScrubber(progress: Int, speed: Float) {
  // 2
  val timeRemaining = ((episodeDuration - progress) / speed).toInt()
  // 3
  if (timeRemaining < 0) {
    return;
  }
  // 4
  progressAnimator = ValueAnimator.ofInt(
      progress, episodeDuration.toInt())
  progressAnimator?.let { animator ->
    // 5
    animator.duration = timeRemaining.toLong()
    // 6
    animator.interpolator = LinearInterpolator()
    // 7
    animator.addUpdateListener {
      if (draggingScrubber) {
        // 8
        animator.cancel()
      } else {
        // 9
        seekBar.progress = animator.animatedValue as Int
      }
    }
    // 10
    animator.start()
  }
}
private fun handleStateChange(state: Int, position: Long, speed: Float) {
val progress = position.toInt()
seekBar.progress = progress
speedButton.text = "${playerSpeed}x"

if (isPlaying) {
  animateScrubber(progress, speed)
}
progressAnimator?.let {
  it.cancel()
  progressAnimator = null
}
handleStateChange(state.state, state.position, state.playbackSpeed)
progressAnimator?.cancel()
private fun updateControlsFromController() {
  val fragmentActivity = activity as FragmentActivity
  val controller = MediaControllerCompat.getMediaController(fragmentActivity)
  if (controller != null) {
    val metadata = controller.metadata
    if (metadata != null) {
      handleStateChange(controller.playbackState.state,
          controller.playbackState.position, playerSpeed)
      updateControlsFromMetadata(controller.metadata)
    }
  }
}
updateControlsFromController()
updateControlsFromController()

视频回放

最后一个功能,您将实现视频播放。如果您现在尝试播放视频播客与PodPlay,只有音频部分将发挥。

识别视频

您需要的第一件事是识别剧集媒体是否是视频的方法。打开 PodcastViewModel.kt. 并添加以下内容 episodeviewdata.:

  var isVideo: Boolean = false
data class EpisodeViewData (
    var guid: String? = "",
    var title: String? = "",
    var description: String? = "",
    var mediaUrl: String? = "",
    var releaseDate: Date? = null,
    var duration: String? = "",
    var isVideo: Boolean = false      
)
return episodes.map {
  val isVideo = it.mimeType.startsWith("video")
  EpisodeViewData(it.guid, it.title, it.description, 
      it.mediaUrl, it.releaseDate, it.duration, isVideo)
}

媒体会议

你需要一个 MediaSession. 对象管理视频播放。

private var mediaSession: MediaSessionCompat? = null
private fun initMediaSession() {
  if (mediaSession == null) {
    // 1
    mediaSession = MediaSessionCompat(activity as Context, 
        "epiSodePlayerFragment.")
    // 2
    mediaSession?.setMediaButtonReceiver(null)
  }
  registerMediaController(mediaSession!!.sessionToken)
}

媒体播放器

你也需要一个 媒体播放器 对象就像你一样 MediaBrowserService.。添加以下属性 epiSodePlayerFragment.:

private var mediaPlayer: MediaPlayer? = null
private var playOnPrepare: Boolean = false
private fun setSurfaceSize() {
  // 1
  val mediaPlayer = mediaPlayer ?: return
  // 2
  val videoWidth = mediaPlayer.videoWidth
  val videoHeight = mediaPlayer.videoHeight
  // 3
  val parent = videoSurfaceView.parent as View
  val containerWidth = parent.width
  val containerHeight = parent.height
  // 4
  val layoutAspectRatio = containerWidth.toFloat() / 
      containerHeight
  val videoAspectRatio = videoWidth.toFloat() / videoHeight
  // 5
  val layoutParams = videoSurfaceView.layoutParams
  // 6
  if (videoAspectRatio > layoutAspectRatio) {
    layoutParams.height = 
        (containerWidth / videoAspectRatio).toInt()
  } else {
    layoutParams.width = 
        (containerHeight * videoAspectRatio).toInt()
  }
  // 7
  videoSurfaceView.layoutParams = layoutParams
}
private fun initMediaPlayer() {
  if (mediaPlayer == null) {
    // 1
    mediaPlayer = MediaPlayer()
    mediaPlayer?.let {
      // 2
      it.setAudioStreamType(AudioManager.STREAM_MUSIC)
      // 3
      it.setDataSource(
          podcastViewModel.activeEpisodeViewData?.mediaUrl)
      // 4
      it.setOnPreparedListener {
        // 5
        val fragmentActivity = activity as FragmentActivity
        val episodeMediaCallback = PodplayMediaCallback(
            fragmentActivity, mediaSession!!, it)
        mediaSession!!.setCallback(episodeMediaCallback)
        // 6
        setSurfaceSize()
        // 7
        if (playOnPrepare) {
          togglePlayPause()
        }
      }
      // 8
      it.prepareAsync()
    }
  } else {
    // 9
    setSurfaceSize()
  }
}
playOnPrepare = true
private fun initVideoPlayer() {
  // 1
  videoSurfaceView.visibility = View.VISIBLE
  // 2
  val surfaceHolder = videoSurfaceView.holder
  // 3
  surfaceHolder.addCallback(object: SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
      // 4
      initMediaPlayer()
      mediaPlayer?.setDisplay(holder)
    }
    override fun surfaceChanged(var1: SurfaceHolder, var2: Int,
        var3: Int, var4: Int) {
    }
    override fun surfaceDestroyed(var1: SurfaceHolder) {
    }
  })
}

Surfaceview.概述

This method warrants some explanation on how surface views interact with the media player. To display videos, the 媒体播放器 object requires access to a Surfaceview.。曲面视图在视图层次结构中提供专用的绘图表面。

private var isVideo: Boolean = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  isVideo = podcastViewModel.activeEpisodeViewData?.isVideo ?: false
} else {
  isVideo = false
}
if (!isVideo) {
  initMediaBrowser()
}
if (!isVideo) {
  if (mediaBrowser.isConnected) {
    val fragmentActivity = activity as FragmentActivity
    if (MediaControllerCompat.getMediaController(
        fragmentActivity) == null) {
      registerMediaController(mediaBrowser.sessionToken)
    }
    updateControlsFromController()
  } else {
    mediaBrowser.connect()
  }
}
if (isVideo) {
  mediaPlayer?.setDisplay(null)
}
if (isVideo) {
  initMediaSession()
  initVideoPlayer()
}
private fun setupVideoUI() {
  episodeDescTextView.visibility = View.INVISIBLE
  headerView.visibility = View.INVISIBLE
  val activity = activity as AppCompatActivity
  activity.supportActionBar?.hide()
  playerControls.setBackgroundColor(Color.argb(255/2, 0, 0, 0))
}
if (isVideo) {
  setupVideoUI()
}
if (!fragmentActivity.isChangingConfigurations) {
  mediaPlayer?.release()
  mediaPlayer = null
}
我diaPlayer?.let {
  updateControlsFromController()
}
private var mediaNeedsPrepare: Boolean = false
我diaNeedsPrepare = true
我diaPlayer.reset()
mediaPlayer.setDataSource(context, mediaUri)
mediaPlayer.prepare()
if (mediaNeedsPrepare) {
  mediaPlayer.reset()
  mediaPlayer.setDataSource(context, mediaUri)
  mediaPlayer.prepare()
}

然后去哪儿?

恭喜,你现在有一个齐全的播客球员值得赞美和吹牛的权利!拍手,因为你已经完成了很多东西。

有技术问题吗?要报告bug? 您可以向官方书籍论坛中的书籍作者提出问题和报告错误 这里.

Hage反馈分享在线阅读体验? 如果您对UI,UX,高亮反馈,或者我们的在线读者的其他功能,您可以将它们发送给设计团队与下面的表格:

© 2021 Razeware LLC

您可以免费读取,本章的部分显示为 混淆了 文本。解锁这本书,以及我们整个书籍和视频目录,带有Raywenderlich.com的专业订阅。

现在解锁

要突出或记笔记,您需要在订阅中拥有这本书或自行购买。