Skip to content

Commit c34170f

Browse files
committed
✨ feat: done open preview on Android
1 parent a996d84 commit c34170f

File tree

7 files changed

+258
-21
lines changed

7 files changed

+258
-21
lines changed

android/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ dependencies {
135135
// simple camerax library (Not necessary)
136136
implementation 'io.github.lucksiege:camerax:v3.11.2'
137137

138+
// exoplayer
139+
implementation "com.google.android.exoplayer:exoplayer:2.19.1"
140+
138141

139142
implementation "com.facebook.react:react-native:+"
140143

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.margelo.nitro.multipleimagepicker
2+
3+
import android.content.Context
4+
import android.net.Uri
5+
import android.view.View
6+
import com.google.android.exoplayer2.ExoPlayer
7+
import com.google.android.exoplayer2.MediaItem
8+
import com.google.android.exoplayer2.PlaybackException
9+
import com.google.android.exoplayer2.Player
10+
import com.google.android.exoplayer2.ui.StyledPlayerView
11+
import com.luck.picture.lib.config.PictureMimeType
12+
import com.luck.picture.lib.config.SelectorProviders
13+
import com.luck.picture.lib.engine.VideoPlayerEngine
14+
import com.luck.picture.lib.entity.LocalMedia
15+
import com.luck.picture.lib.interfaces.OnPlayerListener
16+
import java.io.File
17+
import java.util.concurrent.CopyOnWriteArrayList
18+
19+
20+
class ExoPlayerEngine : VideoPlayerEngine<StyledPlayerView> {
21+
private val listeners = CopyOnWriteArrayList<OnPlayerListener>()
22+
23+
override fun onCreateVideoPlayer(context: Context): View {
24+
val exoPlayer = StyledPlayerView(context)
25+
exoPlayer.useController = true
26+
return exoPlayer
27+
}
28+
29+
override fun onStarPlayer(exoPlayer: StyledPlayerView, media: LocalMedia) {
30+
val player = exoPlayer.player
31+
if (player != null) {
32+
val mediaItem: MediaItem
33+
val path = media.availablePath
34+
mediaItem = if (PictureMimeType.isContent(path)) {
35+
MediaItem.fromUri(Uri.parse(path))
36+
} else if (PictureMimeType.isHasHttp(path)) {
37+
MediaItem.fromUri(path)
38+
} else {
39+
MediaItem.fromUri(Uri.fromFile(File(path)))
40+
}
41+
val config = SelectorProviders.getInstance().selectorConfig
42+
player.repeatMode =
43+
if (config.isLoopAutoPlay) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
44+
player.setMediaItem(mediaItem)
45+
player.prepare()
46+
player.play()
47+
}
48+
}
49+
50+
override fun onResume(exoPlayer: StyledPlayerView) {
51+
val player = exoPlayer.player
52+
player?.play()
53+
}
54+
55+
override fun onPause(exoPlayer: StyledPlayerView) {
56+
val player = exoPlayer.player
57+
player?.pause()
58+
}
59+
60+
override fun isPlaying(exoPlayer: StyledPlayerView): Boolean {
61+
val player = exoPlayer.player
62+
return player != null && player.isPlaying
63+
}
64+
65+
66+
override fun addPlayListener(playerListener: OnPlayerListener) {
67+
if (!listeners.contains(playerListener)) {
68+
listeners.add(playerListener)
69+
}
70+
}
71+
72+
override fun removePlayListener(playerListener: OnPlayerListener) {
73+
listeners.remove(playerListener)
74+
}
75+
76+
override fun onPlayerAttachedToWindow(exoPlayer: StyledPlayerView) {
77+
val player: Player = ExoPlayer.Builder(exoPlayer.context).build()
78+
exoPlayer.player = player
79+
player.addListener(mPlayerListener)
80+
}
81+
82+
override fun onPlayerDetachedFromWindow(exoPlayer: StyledPlayerView) {
83+
val player = exoPlayer.player
84+
if (player != null) {
85+
player.removeListener(mPlayerListener)
86+
player.release()
87+
exoPlayer.player = null
88+
}
89+
}
90+
91+
override fun destroy(exoPlayer: StyledPlayerView) {
92+
val player = exoPlayer.player
93+
if (player != null) {
94+
player.removeListener(mPlayerListener)
95+
player.release()
96+
}
97+
}
98+
99+
private val mPlayerListener: Player.Listener = object : Player.Listener {
100+
override fun onPlayerError(error: PlaybackException) {
101+
for (i in listeners.indices) {
102+
val playerListener = listeners[i]
103+
playerListener.onPlayerError()
104+
}
105+
}
106+
107+
override fun onPlaybackStateChanged(playbackState: Int) {
108+
if (playbackState == Player.STATE_READY) {
109+
for (i in listeners.indices) {
110+
val playerListener = listeners[i]
111+
playerListener.onPlayerReady()
112+
}
113+
} else if (playbackState == Player.STATE_BUFFERING) {
114+
for (i in listeners.indices) {
115+
val playerListener = listeners[i]
116+
playerListener.onPlayerLoading()
117+
}
118+
} else if (playbackState == Player.STATE_ENDED) {
119+
for (i in listeners.indices) {
120+
val playerListener = listeners[i]
121+
playerListener.onPlayerEnd()
122+
}
123+
}
124+
}
125+
}
126+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.margelo.nitro.multipleimagepicker
2+
3+
4+
import android.app.Dialog
5+
import android.content.Context
6+
import android.os.Bundle
7+
import android.view.Gravity
8+
import android.view.ViewGroup
9+
10+
11+
class LoadingDialog(context: Context?) :
12+
Dialog(context!!, R.style.Picture_Theme_AlertDialog) {
13+
init {
14+
setCancelable(true)
15+
setCanceledOnTouchOutside(false)
16+
}
17+
18+
override fun onCreate(savedInstanceState: Bundle) {
19+
super.onCreate(savedInstanceState)
20+
setContentView(R.layout.loading_dialog)
21+
setDialogSize()
22+
}
23+
24+
private fun setDialogSize() {
25+
val params = window!!.attributes
26+
params.width = ViewGroup.LayoutParams.WRAP_CONTENT
27+
params.height = ViewGroup.LayoutParams.WRAP_CONTENT
28+
params.gravity = Gravity.CENTER
29+
window!!.setWindowAnimations(R.style.PictureThemeDialogWindowStyle)
30+
window!!.attributes = params
31+
}
32+
}

android/src/main/java/com/margelo/nitro/multipleimagepicker/MultipleImagePickerImp.kt

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package com.margelo.nitro.multipleimagepicker
22

33
import android.app.Activity
4-
import android.content.ContentResolver
54
import android.content.Context
65
import android.content.Intent
76
import android.graphics.Color
87
import android.net.Uri
98
import androidx.core.content.ContextCompat
10-
import com.facebook.react.bridge.ActivityEventListener
119
import com.facebook.react.bridge.BaseActivityEventListener
1210
import com.facebook.react.bridge.ColorPropConverter
1311
import com.facebook.react.bridge.ReactApplicationContext
@@ -19,9 +17,9 @@ import com.luck.picture.lib.basic.PictureSelector
1917
import com.luck.picture.lib.config.PictureMimeType
2018
import com.luck.picture.lib.config.SelectMimeType
2119
import com.luck.picture.lib.config.SelectModeConfig
22-
import com.luck.picture.lib.engine.ImageEngine
2320
import com.luck.picture.lib.engine.PictureSelectorEngine
2421
import com.luck.picture.lib.entity.LocalMedia
22+
import com.luck.picture.lib.interfaces.OnCustomLoadingListener
2523
import com.luck.picture.lib.interfaces.OnMediaEditInterceptListener
2624
import com.luck.picture.lib.interfaces.OnResultCallbackListener
2725
import com.luck.picture.lib.language.LanguageConfig
@@ -32,6 +30,7 @@ import com.luck.picture.lib.style.SelectMainStyle
3230
import com.luck.picture.lib.style.TitleBarStyle
3331
import com.luck.picture.lib.utils.DateUtils
3432
import com.luck.picture.lib.utils.DensityUtil
33+
import com.luck.picture.lib.utils.MediaUtils
3534
import com.yalantis.ucrop.UCrop
3635
import com.yalantis.ucrop.UCrop.Options
3736
import com.yalantis.ucrop.UCrop.REQUEST_CROP
@@ -99,7 +98,8 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
9998
.openGallery(chooseMode)
10099
.setImageEngine(imageEngine)
101100
.setSelectedData(dataList)
102-
.setSelectorUIStyle(style).apply {
101+
.setSelectorUIStyle(style)
102+
.apply {
103103
if (isCrop) {
104104
setCropOption(config.crop)
105105
// Disabled force crop engine for multiple
@@ -126,7 +126,8 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
126126
if (videoQuality != null && videoQuality != 1.0) {
127127
setVideoQuality(if (videoQuality > 0.5) 1 else 0)
128128
}
129-
}.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3)
129+
}
130+
.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3)
130131
.setMaxSelectNum(maxSelect)
131132
.isDirectReturnSingle(true)
132133
.isSelectZoomAnim(true)
@@ -264,17 +265,60 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
264265

265266
@ReactMethod
266267
fun openPreview(media: Array<MediaPreview>, config: NitroPreviewConfig) {
267-
268268
val imageEngine = GlideEngine.createGlideEngine()
269269

270+
val assets: ArrayList<LocalMedia> = arrayListOf()
271+
272+
val previewStyle = PictureSelectorStyle()
273+
val titleBarStyle = TitleBarStyle()
274+
275+
previewStyle.windowAnimationStyle.setActivityEnterAnimation(R.anim.anim_modal_in)
276+
previewStyle.windowAnimationStyle.setActivityExitAnimation(com.luck.picture.lib.R.anim.ps_anim_modal_out)
277+
previewStyle.selectMainStyle.previewBackgroundColor = Color.BLACK
278+
279+
titleBarStyle.previewTitleBackgroundColor = Color.BLACK
280+
previewStyle.titleBarStyle = titleBarStyle
281+
282+
media.forEach { mediaItem ->
283+
var asset: LocalMedia? = null
284+
285+
mediaItem.path?.let { path ->
286+
// network asset
287+
if (path.startsWith("https://") || path.startsWith("http://")) {
288+
val localMedia = LocalMedia.create()
289+
localMedia.path = path
290+
localMedia.mimeType =
291+
if (mediaItem.type == ResultType.VIDEO) "video/mp4" else MediaUtils.getMimeTypeFromMediaHttpUrl(
292+
path
293+
) ?: "image/jpg"
294+
asset = localMedia
295+
} else {
296+
asset = LocalMedia.generateLocalMedia(appContext, path)
297+
}
298+
}
299+
300+
asset?.let { assets.add(it) }
301+
}
302+
270303
PictureSelector
271304
.create(currentActivity)
272305
.openPreview()
273306
.setImageEngine(imageEngine)
274307
.setLanguage(getLanguage(config.language))
275-
.
308+
.setSelectorUIStyle(previewStyle)
309+
.isPreviewFullScreenMode(true)
310+
.isAutoVideoPlay(true)
311+
.setVideoPlayerEngine(ExoPlayerEngine())
312+
.isVideoPauseResumePlay(true)
313+
.setCustomLoadingListener(getCustomLoadingListener())
314+
.startActivityPreview(config.index.toInt(), false, assets)
315+
}
316+
317+
private fun getCustomLoadingListener(): OnCustomLoadingListener {
318+
return OnCustomLoadingListener { context -> LoadingDialog(context) }
276319
}
277320

321+
278322
private fun getLanguage(language: Language): Int {
279323
return when (language) {
280324
Language.VI -> LanguageConfig.VIETNAM // -> 🇻🇳 My country. Yeahhh
@@ -341,9 +385,7 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
341385
0,
342386
*ratioList.take(5).toTypedArray()
343387
)
344-
345388
}
346-
347389
}
348390
}
349391

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<set xmlns:android="http://schemas.android.com/apk/res/android"
4+
android:interpolator="@android:anim/linear_interpolator"
5+
android:shareInterpolator="true">
6+
<alpha
7+
android:fromAlpha="0.2"
8+
android:toAlpha="1"
9+
android:duration="90"/>
10+
11+
<scale
12+
android:fromXScale="0.7"
13+
android:toXScale="1.0"
14+
android:fromYScale="0.7"
15+
android:toYScale="1.0"
16+
android:pivotX="50%"
17+
android:pivotY="50%"
18+
android:duration="135"/>
19+
</set>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:id="@+id/loading"
4+
android:layout_width="wrap_content"
5+
android:layout_height="wrap_content"
6+
android:background="@drawable/ps_dialog_loading_bg"
7+
android:orientation="vertical"
8+
android:padding="10dp">
9+
10+
<ProgressBar
11+
android:layout_width="25dp"
12+
android:layout_height="25dp"
13+
android:layout_gravity="center_horizontal"
14+
android:indeterminateBehavior="repeat"
15+
16+
/>
17+
18+
</LinearLayout>

example/src/index.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ export default function App() {
7979
openPreview(
8080
[
8181
{
82-
path: 'http://tsnrhapp.oss-cn-hangzhou.aliyuncs.com/chartle/IMG_3385.MP4',
83-
type: 'video',
82+
path: 'https://images.unsplash.com/photo-1733863200891-22bba4483644?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxmZWF0dXJlZC1waG90b3MtZmVlZHw0fHx8ZW58MHx8fHx8',
83+
type: 'image',
8484
} as MediaPreview,
8585
...images,
8686
],
@@ -112,16 +112,13 @@ export default function App() {
112112
const onCrop = async () => {
113113
try {
114114
console.log('images: ', images)
115-
const response = await openCropper(
116-
'https://images.unsplash.com/photo-1610562275255-03b7fa0d4655?q=80&w=2861&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
117-
{
118-
ratio: [
119-
{ title: 'Instagram', width: 1, height: 1 },
120-
{ title: 'Twitter', width: 16, height: 9 },
121-
{ title: 'Facebook', width: 12, height: 11 },
122-
],
123-
}
124-
)
115+
const response = await openCropper(images[0].path, {
116+
ratio: [
117+
{ title: 'Instagram', width: 1, height: 1 },
118+
{ title: 'Twitter', width: 16, height: 9 },
119+
{ title: 'Facebook', width: 12, height: 11 },
120+
],
121+
})
125122

126123
setImages((prev) => {
127124
const data = [...prev]

0 commit comments

Comments
 (0)