kandi background
Explore Kits

PreviewSeekBar | SeekBar suited | Animation library

 by   rubensousa Java Version: exoplayer2.11.4.0 License: Apache-2.0

 by   rubensousa Java Version: exoplayer2.11.4.0 License: Apache-2.0

Download this library from

kandi X-RAY | PreviewSeekBar Summary

PreviewSeekBar is a Java library typically used in Telecommunications, Media, Media, Entertainment, User Interface, Animation applications. PreviewSeekBar has no bugs, it has no vulnerabilities, it has build file available, it has a Permissive License and it has medium support. You can download it from GitHub.
A SeekBar suited for showing a video preview. As seen in Google Play Movies.
Support
Support
Quality
Quality
Security
Security
License
License
Reuse
Reuse

kandi-support Support

  • PreviewSeekBar has a medium active ecosystem.
  • It has 3243 star(s) with 401 fork(s). There are 69 watchers for this library.
  • It had no major release in the last 12 months.
  • There are 2 open issues and 33 have been closed. On average issues are closed in 146 days. There are no pull requests.
  • It has a neutral sentiment in the developer community.
  • The latest version of PreviewSeekBar is exoplayer2.11.4.0
PreviewSeekBar Support
Best in #Animation
Average in #Animation
PreviewSeekBar Support
Best in #Animation
Average in #Animation

quality kandi Quality

  • PreviewSeekBar has 0 bugs and 0 code smells.
PreviewSeekBar Quality
Best in #Animation
Average in #Animation
PreviewSeekBar Quality
Best in #Animation
Average in #Animation

securitySecurity

  • PreviewSeekBar has no vulnerabilities reported, and its dependent libraries have no vulnerabilities reported.
  • PreviewSeekBar code analysis shows 0 unresolved vulnerabilities.
  • There are 0 security hotspots that need review.
PreviewSeekBar Security
Best in #Animation
Average in #Animation
PreviewSeekBar Security
Best in #Animation
Average in #Animation

license License

  • PreviewSeekBar is licensed under the Apache-2.0 License. This license is Permissive.
  • Permissive licenses have the least restrictions, and you can use them in most projects.
PreviewSeekBar License
Best in #Animation
Average in #Animation
PreviewSeekBar License
Best in #Animation
Average in #Animation

buildReuse

  • PreviewSeekBar releases are available to install and integrate.
  • Build file is available. You can build the component from source.
  • Installation instructions, examples and code snippets are available.
  • PreviewSeekBar saves you 821 person hours of effort in developing the same functionality from scratch.
  • It has 1885 lines of code, 172 functions and 36 files.
  • It has medium code complexity. Code complexity directly impacts maintainability of the code.
PreviewSeekBar Reuse
Best in #Animation
Average in #Animation
PreviewSeekBar Reuse
Best in #Animation
Average in #Animation
Top functions reviewed by kandi - BETA

kandi has reviewed PreviewSeekBar and discovered the below as its top functions. This is intended to give you an instant insight into PreviewSeekBar implemented functionality, and help decide if they suit your requirements.

  • Set up the options .
  • Initializes the preview dialog .
  • Initializes the View .
  • Update the x position of the preview .
  • Starts a circular reveal animation .
  • Start circular reveal animation .
  • Get media source .
  • Hides the visibility of the preview bar .
  • Load thumbnail preview .
  • Attaches the preference to the parent view .

PreviewSeekBar Key Features

A SeekBar suited for showing a preview of something. As seen in Google Play Movies.

Build

copy iconCopydownload iconDownload
dependencies {
    // Base implementation with a standard SeekBar
    implementation 'com.github.rubensousa:previewseekbar:3.0.0'

    // ExoPlayer extension that contains a TimeBar. 
    // Grab this one if you're going to integrate with ExoPlayer
    implementation 'com.github.rubensousa:previewseekbar-exoplayer:2.11.4.0'
}

Add a custom controller to your PlayerView

copy iconCopydownload iconDownload
<com.google.android.exoplayer2.ui.PlayerView
    android:id="@+id/playerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:controller_layout_id="@layout/exoplayer_controls"/>

Change your TimeBar to a PreviewTimeBar

copy iconCopydownload iconDownload
<FrameLayout
    android:id="@+id/previewFrameLayout"
    android:layout_width="@dimen/video_preview_width"
    android:layout_height="@dimen/video_preview_height"
    android:background="@drawable/video_frame">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</FrameLayout>

<com.github.rubensousa.previewseekbar.exoplayer.PreviewTimeBar
    android:id="@+id/exo_progress"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginEnd="8dp"
    app:previewAnimationEnabled="true"
    app:previewFrameLayout="@id/previewFrameLayout"/>

Create a PreviewLoader and pass it to the PreviewTimeBar

copy iconCopydownload iconDownload
PreviewLoader imagePreviewLoader = ImagePreviewLoader();

previewTimeBar.setPreviewLoader(imagePreviewLoader);

Listen for scrub events to control playback state

copy iconCopydownload iconDownload
previewTimeBar.addOnScrubListener(new PreviewBar.OnScrubListener() {
    @Override
    public void onScrubStart(PreviewBar previewBar) {
        player.setPlayWhenReady(false);
    }

    @Override
    public void onScrubMove(PreviewBar previewBar, int progress, boolean fromUser) {
        
    }

    @Override
    public void onScrubStop(PreviewBar previewBar) {
        player.setPlayWhenReady(true);
    }
});

Customize the PreviewTimeBar

copy iconCopydownload iconDownload
<attr name="previewAnimationEnabled" format="boolean" />
<attr name="previewEnabled" format="boolean" />
<attr name="previewAutoHide" format="boolean" />

Setup your layout like the following:

copy iconCopydownload iconDownload
<FrameLayout
  android:id="@+id/previewFrameLayout"
  android:layout_width="160dp"
  android:layout_height="90dp">

  <ImageView
      android:id="@+id/imageView"
      android:layout_width="match_parent"
      android:layout_height="match_parent" />

</FrameLayout>

<com.github.rubensousa.previewseekbar.PreviewSeekBar
  android:id="@+id/previewSeekBar"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_marginTop="24dp"
  android:max="800"
  app:previewFrameLayout="@id/previewFrameLayout"/>

Create a PreviewLoader and pass it to the PreviewSeekBar

copy iconCopydownload iconDownload

PreviewSeekBar previewSeekBar = findViewById(R.id.previewSeekBar);

PreviewLoader imagePreviewLoader = ImagePreviewLoader();

previewSeekbar.setPreviewLoader(imagePreviewLoader);

Customization

copy iconCopydownload iconDownload
<attr name="previewAnimationEnabled" format="boolean" />
<attr name="previewEnabled" format="boolean" />
<attr name="previewThumbTint" format="color" />
<attr name="previewAutoHide" format="boolean" />

License

copy iconCopydownload iconDownload
Copyright 2017 The Android Open Source Project
Copyright 2020 Rúben Sousa

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Exoplayer seekbar preview

copy iconCopydownload iconDownload
private const val MAX_LINES = 6
private const val MAX_COLUMNS = 10
private const val THUMBNAILS_EACH = 1000 // milliseconds
private const val ONE_MINUTE = 60000 // one minute in millisecond

class GlideThumbnailTransformation(position: Long) : BitmapTransformation() {

    private val x: Int
    private val y: Int

    init {
        // Remainder of position on one minute because we just need to know which square of the current miniature
        val square = position.rem(ONE_MINUTE).toInt() / THUMBNAILS_EACH
        y = square / MAX_COLUMNS
        x = square % MAX_COLUMNS
    }

    override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val width = toTransform.width / MAX_COLUMNS
        val height = toTransform.height / MAX_LINES
        return Bitmap.createBitmap(toTransform, x * width, y * height, width, height)
    }

    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        val data: ByteArray = ByteBuffer.allocate(8).putInt(x).putInt(y).array()
        messageDigest.update(data)
    }

    override fun hashCode(): Int {
        return (x.toString() + y.toString()).hashCode()
    }

    override fun equals(other: Any?): Boolean {
        if (other !is GlideThumbnailTransformation) {
            return false
        }
        return other.x == x && other.y == y
    }
}
exo_progress.addListener(object : TimeBar.OnScrubListener {
    override fun onScrubMove(timeBar: TimeBar, position: Long) {
        previewFrameLayout.visibility = View.VISIBLE
        val thumbnailUrl = "${MOSAIQUE_URL}${position.div(60000)}.jpg"
        val targetX = updatePreviewX(position.toInt(), exoPlayer.duration.toInt())
        previewFrameLayout.x = targetX.toFloat()
        GlideApp.with(scrubbingPreview)
            .load(thumbnailUrl)
            .override(Target.SIZE_ORIGINAL,Target.SIZE_ORIGINAL)
            .transform(GlideThumbnailTransformation(position))
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .into(scrubbingPreview)
    }

    override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
        previewFrameLayout.visibility = View.INVISIBLE
    }

    override fun onScrubStart(timeBar: TimeBar, position: Long) {}
})

private fun updatePreviewX(progress: Int, max: Int): Int {
    if (max == 0) { return 0 }

    val parent = previewFrameLayout.parent as ViewGroup
    val layoutParams = previewFrameLayout.layoutParams as MarginLayoutParams
    val offset = progress.toFloat() / max
    val minimumX: Int = previewFrameLayout.left
    val maximumX = (parent.width - parent.paddingRight - layoutParams.rightMargin)

// We remove the padding of the scrubbing, if you have a custom size juste use dimen to calculate this
    val previewPaddingRadius: Int = dpToPx(resources.displayMetrics, DefaultTimeBar.DEFAULT_SCRUBBER_DRAGGED_SIZE_DP).div(2)
    val previewLeftX = (exo_progress as View).left.toFloat()
    val previewRightX = (exo_progress as View).right.toFloat()
    val previewSeekBarStartX: Float = previewLeftX + previewPaddingRadius
    val previewSeekBarEndX: Float = previewRightX - previewPaddingRadius
    val currentX = (previewSeekBarStartX + (previewSeekBarEndX - previewSeekBarStartX) * offset)
    val startX: Float = currentX - previewFrameLayout.width / 2f
    val endX: Float = startX + previewFrameLayout.width

    // Clamp the moves
    return if (startX >= minimumX && endX <= maximumX) {
        startX.toInt()
    } else if (startX < minimumX) {
        minimumX
    } else {
        maximumX - previewFrameLayout.width
    }
}

private fun dpToPx(displayMetrics: DisplayMetrics, dps: Int): Int {
    return (dps * displayMetrics.density).toInt()
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_gravity="bottom"
    android:layoutDirection="ltr"
    android:background="#CC000000"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingTop="4dp"
        android:orientation="horizontal"
        android:id="@+id/controlsLayout"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageButton android:id="@id/exo_prev"
            style="@style/ExoMediaButton.Previous"/>

        <ImageButton android:id="@id/exo_rew"
            style="@style/ExoMediaButton.Rewind"/>

        <ImageButton android:id="@id/exo_repeat_toggle"
            style="@style/ExoMediaButton"/>

        <ImageButton android:id="@id/exo_play"
            style="@style/ExoMediaButton.Play"/>

        <ImageButton android:id="@id/exo_pause"
            style="@style/ExoMediaButton.Pause"/>

        <ImageButton android:id="@id/exo_ffwd"
            style="@style/ExoMediaButton.FastForward"/>

        <ImageButton android:id="@id/exo_next"
            style="@style/ExoMediaButton.Next"/>

    </LinearLayout>

    <TextView android:id="@id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBottom_toTopOf="@id/controlsLayout"
        app:layout_constraintStart_toStartOf="parent"/>

    <FrameLayout
        android:id="@+id/previewFrameLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:background="@drawable/video_frame"
        android:padding="2dp"
        android:visibility="invisible"
        app:layout_constraintBottom_toTopOf="@+id/exo_progress"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.25"
        tools:visibility="visible">

        <ImageView
            android:id="@+id/scrubbingPreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY" />

    </FrameLayout>

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@id/exo_progress"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="26dp"
        app:layout_constraintBottom_toBottomOf="@id/exo_position"
        app:layout_constraintEnd_toStartOf="@id/exo_duration"
        app:layout_constraintStart_toEndOf="@+id/exo_position"
        app:layout_constraintTop_toTopOf="@+id/exo_position"/>

    <TextView android:id="@id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <stroke
        android:width="2dp"
        android:color="@android:color/white" />

    <solid android:color="@android:color/black" />
</shape>
-----------------------
private const val MAX_LINES = 6
private const val MAX_COLUMNS = 10
private const val THUMBNAILS_EACH = 1000 // milliseconds
private const val ONE_MINUTE = 60000 // one minute in millisecond

class GlideThumbnailTransformation(position: Long) : BitmapTransformation() {

    private val x: Int
    private val y: Int

    init {
        // Remainder of position on one minute because we just need to know which square of the current miniature
        val square = position.rem(ONE_MINUTE).toInt() / THUMBNAILS_EACH
        y = square / MAX_COLUMNS
        x = square % MAX_COLUMNS
    }

    override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val width = toTransform.width / MAX_COLUMNS
        val height = toTransform.height / MAX_LINES
        return Bitmap.createBitmap(toTransform, x * width, y * height, width, height)
    }

    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        val data: ByteArray = ByteBuffer.allocate(8).putInt(x).putInt(y).array()
        messageDigest.update(data)
    }

    override fun hashCode(): Int {
        return (x.toString() + y.toString()).hashCode()
    }

    override fun equals(other: Any?): Boolean {
        if (other !is GlideThumbnailTransformation) {
            return false
        }
        return other.x == x && other.y == y
    }
}
exo_progress.addListener(object : TimeBar.OnScrubListener {
    override fun onScrubMove(timeBar: TimeBar, position: Long) {
        previewFrameLayout.visibility = View.VISIBLE
        val thumbnailUrl = "${MOSAIQUE_URL}${position.div(60000)}.jpg"
        val targetX = updatePreviewX(position.toInt(), exoPlayer.duration.toInt())
        previewFrameLayout.x = targetX.toFloat()
        GlideApp.with(scrubbingPreview)
            .load(thumbnailUrl)
            .override(Target.SIZE_ORIGINAL,Target.SIZE_ORIGINAL)
            .transform(GlideThumbnailTransformation(position))
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .into(scrubbingPreview)
    }

    override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
        previewFrameLayout.visibility = View.INVISIBLE
    }

    override fun onScrubStart(timeBar: TimeBar, position: Long) {}
})

private fun updatePreviewX(progress: Int, max: Int): Int {
    if (max == 0) { return 0 }

    val parent = previewFrameLayout.parent as ViewGroup
    val layoutParams = previewFrameLayout.layoutParams as MarginLayoutParams
    val offset = progress.toFloat() / max
    val minimumX: Int = previewFrameLayout.left
    val maximumX = (parent.width - parent.paddingRight - layoutParams.rightMargin)

// We remove the padding of the scrubbing, if you have a custom size juste use dimen to calculate this
    val previewPaddingRadius: Int = dpToPx(resources.displayMetrics, DefaultTimeBar.DEFAULT_SCRUBBER_DRAGGED_SIZE_DP).div(2)
    val previewLeftX = (exo_progress as View).left.toFloat()
    val previewRightX = (exo_progress as View).right.toFloat()
    val previewSeekBarStartX: Float = previewLeftX + previewPaddingRadius
    val previewSeekBarEndX: Float = previewRightX - previewPaddingRadius
    val currentX = (previewSeekBarStartX + (previewSeekBarEndX - previewSeekBarStartX) * offset)
    val startX: Float = currentX - previewFrameLayout.width / 2f
    val endX: Float = startX + previewFrameLayout.width

    // Clamp the moves
    return if (startX >= minimumX && endX <= maximumX) {
        startX.toInt()
    } else if (startX < minimumX) {
        minimumX
    } else {
        maximumX - previewFrameLayout.width
    }
}

private fun dpToPx(displayMetrics: DisplayMetrics, dps: Int): Int {
    return (dps * displayMetrics.density).toInt()
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_gravity="bottom"
    android:layoutDirection="ltr"
    android:background="#CC000000"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingTop="4dp"
        android:orientation="horizontal"
        android:id="@+id/controlsLayout"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageButton android:id="@id/exo_prev"
            style="@style/ExoMediaButton.Previous"/>

        <ImageButton android:id="@id/exo_rew"
            style="@style/ExoMediaButton.Rewind"/>

        <ImageButton android:id="@id/exo_repeat_toggle"
            style="@style/ExoMediaButton"/>

        <ImageButton android:id="@id/exo_play"
            style="@style/ExoMediaButton.Play"/>

        <ImageButton android:id="@id/exo_pause"
            style="@style/ExoMediaButton.Pause"/>

        <ImageButton android:id="@id/exo_ffwd"
            style="@style/ExoMediaButton.FastForward"/>

        <ImageButton android:id="@id/exo_next"
            style="@style/ExoMediaButton.Next"/>

    </LinearLayout>

    <TextView android:id="@id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBottom_toTopOf="@id/controlsLayout"
        app:layout_constraintStart_toStartOf="parent"/>

    <FrameLayout
        android:id="@+id/previewFrameLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:background="@drawable/video_frame"
        android:padding="2dp"
        android:visibility="invisible"
        app:layout_constraintBottom_toTopOf="@+id/exo_progress"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.25"
        tools:visibility="visible">

        <ImageView
            android:id="@+id/scrubbingPreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY" />

    </FrameLayout>

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@id/exo_progress"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="26dp"
        app:layout_constraintBottom_toBottomOf="@id/exo_position"
        app:layout_constraintEnd_toStartOf="@id/exo_duration"
        app:layout_constraintStart_toEndOf="@+id/exo_position"
        app:layout_constraintTop_toTopOf="@+id/exo_position"/>

    <TextView android:id="@id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <stroke
        android:width="2dp"
        android:color="@android:color/white" />

    <solid android:color="@android:color/black" />
</shape>
-----------------------
private const val MAX_LINES = 6
private const val MAX_COLUMNS = 10
private const val THUMBNAILS_EACH = 1000 // milliseconds
private const val ONE_MINUTE = 60000 // one minute in millisecond

class GlideThumbnailTransformation(position: Long) : BitmapTransformation() {

    private val x: Int
    private val y: Int

    init {
        // Remainder of position on one minute because we just need to know which square of the current miniature
        val square = position.rem(ONE_MINUTE).toInt() / THUMBNAILS_EACH
        y = square / MAX_COLUMNS
        x = square % MAX_COLUMNS
    }

    override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val width = toTransform.width / MAX_COLUMNS
        val height = toTransform.height / MAX_LINES
        return Bitmap.createBitmap(toTransform, x * width, y * height, width, height)
    }

    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        val data: ByteArray = ByteBuffer.allocate(8).putInt(x).putInt(y).array()
        messageDigest.update(data)
    }

    override fun hashCode(): Int {
        return (x.toString() + y.toString()).hashCode()
    }

    override fun equals(other: Any?): Boolean {
        if (other !is GlideThumbnailTransformation) {
            return false
        }
        return other.x == x && other.y == y
    }
}
exo_progress.addListener(object : TimeBar.OnScrubListener {
    override fun onScrubMove(timeBar: TimeBar, position: Long) {
        previewFrameLayout.visibility = View.VISIBLE
        val thumbnailUrl = "${MOSAIQUE_URL}${position.div(60000)}.jpg"
        val targetX = updatePreviewX(position.toInt(), exoPlayer.duration.toInt())
        previewFrameLayout.x = targetX.toFloat()
        GlideApp.with(scrubbingPreview)
            .load(thumbnailUrl)
            .override(Target.SIZE_ORIGINAL,Target.SIZE_ORIGINAL)
            .transform(GlideThumbnailTransformation(position))
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .into(scrubbingPreview)
    }

    override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
        previewFrameLayout.visibility = View.INVISIBLE
    }

    override fun onScrubStart(timeBar: TimeBar, position: Long) {}
})

private fun updatePreviewX(progress: Int, max: Int): Int {
    if (max == 0) { return 0 }

    val parent = previewFrameLayout.parent as ViewGroup
    val layoutParams = previewFrameLayout.layoutParams as MarginLayoutParams
    val offset = progress.toFloat() / max
    val minimumX: Int = previewFrameLayout.left
    val maximumX = (parent.width - parent.paddingRight - layoutParams.rightMargin)

// We remove the padding of the scrubbing, if you have a custom size juste use dimen to calculate this
    val previewPaddingRadius: Int = dpToPx(resources.displayMetrics, DefaultTimeBar.DEFAULT_SCRUBBER_DRAGGED_SIZE_DP).div(2)
    val previewLeftX = (exo_progress as View).left.toFloat()
    val previewRightX = (exo_progress as View).right.toFloat()
    val previewSeekBarStartX: Float = previewLeftX + previewPaddingRadius
    val previewSeekBarEndX: Float = previewRightX - previewPaddingRadius
    val currentX = (previewSeekBarStartX + (previewSeekBarEndX - previewSeekBarStartX) * offset)
    val startX: Float = currentX - previewFrameLayout.width / 2f
    val endX: Float = startX + previewFrameLayout.width

    // Clamp the moves
    return if (startX >= minimumX && endX <= maximumX) {
        startX.toInt()
    } else if (startX < minimumX) {
        minimumX
    } else {
        maximumX - previewFrameLayout.width
    }
}

private fun dpToPx(displayMetrics: DisplayMetrics, dps: Int): Int {
    return (dps * displayMetrics.density).toInt()
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_gravity="bottom"
    android:layoutDirection="ltr"
    android:background="#CC000000"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingTop="4dp"
        android:orientation="horizontal"
        android:id="@+id/controlsLayout"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageButton android:id="@id/exo_prev"
            style="@style/ExoMediaButton.Previous"/>

        <ImageButton android:id="@id/exo_rew"
            style="@style/ExoMediaButton.Rewind"/>

        <ImageButton android:id="@id/exo_repeat_toggle"
            style="@style/ExoMediaButton"/>

        <ImageButton android:id="@id/exo_play"
            style="@style/ExoMediaButton.Play"/>

        <ImageButton android:id="@id/exo_pause"
            style="@style/ExoMediaButton.Pause"/>

        <ImageButton android:id="@id/exo_ffwd"
            style="@style/ExoMediaButton.FastForward"/>

        <ImageButton android:id="@id/exo_next"
            style="@style/ExoMediaButton.Next"/>

    </LinearLayout>

    <TextView android:id="@id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBottom_toTopOf="@id/controlsLayout"
        app:layout_constraintStart_toStartOf="parent"/>

    <FrameLayout
        android:id="@+id/previewFrameLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:background="@drawable/video_frame"
        android:padding="2dp"
        android:visibility="invisible"
        app:layout_constraintBottom_toTopOf="@+id/exo_progress"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.25"
        tools:visibility="visible">

        <ImageView
            android:id="@+id/scrubbingPreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY" />

    </FrameLayout>

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@id/exo_progress"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="26dp"
        app:layout_constraintBottom_toBottomOf="@id/exo_position"
        app:layout_constraintEnd_toStartOf="@id/exo_duration"
        app:layout_constraintStart_toEndOf="@+id/exo_position"
        app:layout_constraintTop_toTopOf="@+id/exo_position"/>

    <TextView android:id="@id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <stroke
        android:width="2dp"
        android:color="@android:color/white" />

    <solid android:color="@android:color/black" />
</shape>
-----------------------
private const val MAX_LINES = 6
private const val MAX_COLUMNS = 10
private const val THUMBNAILS_EACH = 1000 // milliseconds
private const val ONE_MINUTE = 60000 // one minute in millisecond

class GlideThumbnailTransformation(position: Long) : BitmapTransformation() {

    private val x: Int
    private val y: Int

    init {
        // Remainder of position on one minute because we just need to know which square of the current miniature
        val square = position.rem(ONE_MINUTE).toInt() / THUMBNAILS_EACH
        y = square / MAX_COLUMNS
        x = square % MAX_COLUMNS
    }

    override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val width = toTransform.width / MAX_COLUMNS
        val height = toTransform.height / MAX_LINES
        return Bitmap.createBitmap(toTransform, x * width, y * height, width, height)
    }

    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        val data: ByteArray = ByteBuffer.allocate(8).putInt(x).putInt(y).array()
        messageDigest.update(data)
    }

    override fun hashCode(): Int {
        return (x.toString() + y.toString()).hashCode()
    }

    override fun equals(other: Any?): Boolean {
        if (other !is GlideThumbnailTransformation) {
            return false
        }
        return other.x == x && other.y == y
    }
}
exo_progress.addListener(object : TimeBar.OnScrubListener {
    override fun onScrubMove(timeBar: TimeBar, position: Long) {
        previewFrameLayout.visibility = View.VISIBLE
        val thumbnailUrl = "${MOSAIQUE_URL}${position.div(60000)}.jpg"
        val targetX = updatePreviewX(position.toInt(), exoPlayer.duration.toInt())
        previewFrameLayout.x = targetX.toFloat()
        GlideApp.with(scrubbingPreview)
            .load(thumbnailUrl)
            .override(Target.SIZE_ORIGINAL,Target.SIZE_ORIGINAL)
            .transform(GlideThumbnailTransformation(position))
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .into(scrubbingPreview)
    }

    override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
        previewFrameLayout.visibility = View.INVISIBLE
    }

    override fun onScrubStart(timeBar: TimeBar, position: Long) {}
})

private fun updatePreviewX(progress: Int, max: Int): Int {
    if (max == 0) { return 0 }

    val parent = previewFrameLayout.parent as ViewGroup
    val layoutParams = previewFrameLayout.layoutParams as MarginLayoutParams
    val offset = progress.toFloat() / max
    val minimumX: Int = previewFrameLayout.left
    val maximumX = (parent.width - parent.paddingRight - layoutParams.rightMargin)

// We remove the padding of the scrubbing, if you have a custom size juste use dimen to calculate this
    val previewPaddingRadius: Int = dpToPx(resources.displayMetrics, DefaultTimeBar.DEFAULT_SCRUBBER_DRAGGED_SIZE_DP).div(2)
    val previewLeftX = (exo_progress as View).left.toFloat()
    val previewRightX = (exo_progress as View).right.toFloat()
    val previewSeekBarStartX: Float = previewLeftX + previewPaddingRadius
    val previewSeekBarEndX: Float = previewRightX - previewPaddingRadius
    val currentX = (previewSeekBarStartX + (previewSeekBarEndX - previewSeekBarStartX) * offset)
    val startX: Float = currentX - previewFrameLayout.width / 2f
    val endX: Float = startX + previewFrameLayout.width

    // Clamp the moves
    return if (startX >= minimumX && endX <= maximumX) {
        startX.toInt()
    } else if (startX < minimumX) {
        minimumX
    } else {
        maximumX - previewFrameLayout.width
    }
}

private fun dpToPx(displayMetrics: DisplayMetrics, dps: Int): Int {
    return (dps * displayMetrics.density).toInt()
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_gravity="bottom"
    android:layoutDirection="ltr"
    android:background="#CC000000"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingTop="4dp"
        android:orientation="horizontal"
        android:id="@+id/controlsLayout"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageButton android:id="@id/exo_prev"
            style="@style/ExoMediaButton.Previous"/>

        <ImageButton android:id="@id/exo_rew"
            style="@style/ExoMediaButton.Rewind"/>

        <ImageButton android:id="@id/exo_repeat_toggle"
            style="@style/ExoMediaButton"/>

        <ImageButton android:id="@id/exo_play"
            style="@style/ExoMediaButton.Play"/>

        <ImageButton android:id="@id/exo_pause"
            style="@style/ExoMediaButton.Pause"/>

        <ImageButton android:id="@id/exo_ffwd"
            style="@style/ExoMediaButton.FastForward"/>

        <ImageButton android:id="@id/exo_next"
            style="@style/ExoMediaButton.Next"/>

    </LinearLayout>

    <TextView android:id="@id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBottom_toTopOf="@id/controlsLayout"
        app:layout_constraintStart_toStartOf="parent"/>

    <FrameLayout
        android:id="@+id/previewFrameLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:background="@drawable/video_frame"
        android:padding="2dp"
        android:visibility="invisible"
        app:layout_constraintBottom_toTopOf="@+id/exo_progress"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.25"
        tools:visibility="visible">

        <ImageView
            android:id="@+id/scrubbingPreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY" />

    </FrameLayout>

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@id/exo_progress"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="26dp"
        app:layout_constraintBottom_toBottomOf="@id/exo_position"
        app:layout_constraintEnd_toStartOf="@id/exo_duration"
        app:layout_constraintStart_toEndOf="@+id/exo_position"
        app:layout_constraintTop_toTopOf="@+id/exo_position"/>

    <TextView android:id="@id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <stroke
        android:width="2dp"
        android:color="@android:color/white" />

    <solid android:color="@android:color/black" />
</shape>

Community Discussions

Trending Discussions on PreviewSeekBar
  • Exoplayer seekbar preview
Trending Discussions on PreviewSeekBar

QUESTION

Exoplayer seekbar preview

Asked 2020-May-28 at 16:24

I'm trying to add a preview to my seekbar on my exoplayer just like in youtube or plex (see the image below)

enter image description here

I've found this library but it isn't up-to-date yet.

I already have the image per frame but I don't know how to integrate them in my Exoplayer, I'm looking for either a tutorial or explanation where I should begin because I'm kind of lost there.

I've found Timebar.onScrubListener while browsing the exoplayer doc. I'm guessing I'll be using these 3 listeners to fetch the position of the scrub and display the corresponding image.

ANSWER

Answered 2020-May-28 at 16:24

UPDATE: The library is up-to-date as of May 2020 so you can use it directly.

I'll leave code below for those who don't want to use the library.


After searching and adapting it to my needs I found a way by looking at how previewSeekBar was doing and I ended up using the same thing so here it is:

My sprite is composed of 10 columns and 6 rows, each square represent 1 second

GlideTransformation

private const val MAX_LINES = 6
private const val MAX_COLUMNS = 10
private const val THUMBNAILS_EACH = 1000 // milliseconds
private const val ONE_MINUTE = 60000 // one minute in millisecond

class GlideThumbnailTransformation(position: Long) : BitmapTransformation() {

    private val x: Int
    private val y: Int

    init {
        // Remainder of position on one minute because we just need to know which square of the current miniature
        val square = position.rem(ONE_MINUTE).toInt() / THUMBNAILS_EACH
        y = square / MAX_COLUMNS
        x = square % MAX_COLUMNS
    }

    override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val width = toTransform.width / MAX_COLUMNS
        val height = toTransform.height / MAX_LINES
        return Bitmap.createBitmap(toTransform, x * width, y * height, width, height)
    }

    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        val data: ByteArray = ByteBuffer.allocate(8).putInt(x).putInt(y).array()
        messageDigest.update(data)
    }

    override fun hashCode(): Int {
        return (x.toString() + y.toString()).hashCode()
    }

    override fun equals(other: Any?): Boolean {
        if (other !is GlideThumbnailTransformation) {
            return false
        }
        return other.x == x && other.y == y
    }
}

Activity

exo_progress.addListener(object : TimeBar.OnScrubListener {
    override fun onScrubMove(timeBar: TimeBar, position: Long) {
        previewFrameLayout.visibility = View.VISIBLE
        val thumbnailUrl = "${MOSAIQUE_URL}${position.div(60000)}.jpg"
        val targetX = updatePreviewX(position.toInt(), exoPlayer.duration.toInt())
        previewFrameLayout.x = targetX.toFloat()
        GlideApp.with(scrubbingPreview)
            .load(thumbnailUrl)
            .override(Target.SIZE_ORIGINAL,Target.SIZE_ORIGINAL)
            .transform(GlideThumbnailTransformation(position))
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .into(scrubbingPreview)
    }

    override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
        previewFrameLayout.visibility = View.INVISIBLE
    }

    override fun onScrubStart(timeBar: TimeBar, position: Long) {}
})

private fun updatePreviewX(progress: Int, max: Int): Int {
    if (max == 0) { return 0 }

    val parent = previewFrameLayout.parent as ViewGroup
    val layoutParams = previewFrameLayout.layoutParams as MarginLayoutParams
    val offset = progress.toFloat() / max
    val minimumX: Int = previewFrameLayout.left
    val maximumX = (parent.width - parent.paddingRight - layoutParams.rightMargin)

// We remove the padding of the scrubbing, if you have a custom size juste use dimen to calculate this
    val previewPaddingRadius: Int = dpToPx(resources.displayMetrics, DefaultTimeBar.DEFAULT_SCRUBBER_DRAGGED_SIZE_DP).div(2)
    val previewLeftX = (exo_progress as View).left.toFloat()
    val previewRightX = (exo_progress as View).right.toFloat()
    val previewSeekBarStartX: Float = previewLeftX + previewPaddingRadius
    val previewSeekBarEndX: Float = previewRightX - previewPaddingRadius
    val currentX = (previewSeekBarStartX + (previewSeekBarEndX - previewSeekBarStartX) * offset)
    val startX: Float = currentX - previewFrameLayout.width / 2f
    val endX: Float = startX + previewFrameLayout.width

    // Clamp the moves
    return if (startX >= minimumX && endX <= maximumX) {
        startX.toInt()
    } else if (startX < minimumX) {
        minimumX
    } else {
        maximumX - previewFrameLayout.width
    }
}

private fun dpToPx(displayMetrics: DisplayMetrics, dps: Int): Int {
    return (dps * displayMetrics.density).toInt()
}

XML

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_gravity="bottom"
    android:layoutDirection="ltr"
    android:background="#CC000000"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingTop="4dp"
        android:orientation="horizontal"
        android:id="@+id/controlsLayout"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageButton android:id="@id/exo_prev"
            style="@style/ExoMediaButton.Previous"/>

        <ImageButton android:id="@id/exo_rew"
            style="@style/ExoMediaButton.Rewind"/>

        <ImageButton android:id="@id/exo_repeat_toggle"
            style="@style/ExoMediaButton"/>

        <ImageButton android:id="@id/exo_play"
            style="@style/ExoMediaButton.Play"/>

        <ImageButton android:id="@id/exo_pause"
            style="@style/ExoMediaButton.Pause"/>

        <ImageButton android:id="@id/exo_ffwd"
            style="@style/ExoMediaButton.FastForward"/>

        <ImageButton android:id="@id/exo_next"
            style="@style/ExoMediaButton.Next"/>

    </LinearLayout>

    <TextView android:id="@id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBottom_toTopOf="@id/controlsLayout"
        app:layout_constraintStart_toStartOf="parent"/>

    <FrameLayout
        android:id="@+id/previewFrameLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:background="@drawable/video_frame"
        android:padding="2dp"
        android:visibility="invisible"
        app:layout_constraintBottom_toTopOf="@+id/exo_progress"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.25"
        tools:visibility="visible">

        <ImageView
            android:id="@+id/scrubbingPreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY" />

    </FrameLayout>

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@id/exo_progress"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="26dp"
        app:layout_constraintBottom_toBottomOf="@id/exo_position"
        app:layout_constraintEnd_toStartOf="@id/exo_duration"
        app:layout_constraintStart_toEndOf="@+id/exo_position"
        app:layout_constraintTop_toTopOf="@+id/exo_position"/>

    <TextView android:id="@id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:includeFontPadding="false"
        android:textColor="#FFBEBEBE"
        app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

drawable/video_frame

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <stroke
        android:width="2dp"
        android:color="@android:color/white" />

    <solid android:color="@android:color/black" />
</shape>

there might be some improvement to make so feel free to comment

Source https://stackoverflow.com/questions/61887784

Community Discussions, Code Snippets contain sources that include Stack Exchange Network

Vulnerabilities

No vulnerabilities reported

Install PreviewSeekBar

Add the following to your app's build.gradle:.
Place the View you want to use to display the preview in the FrameLayout above. In this example it's an ImageView but you can place any View inside. PreviewSeekBar will animate and show that FrameLayout for you automatically.

Support

For any new features, suggestions and bugs create an issue on GitHub. If you have any questions check and ask questions on community page Stack Overflow .

DOWNLOAD this Library from

Find, review, and download reusable Libraries, Code Snippets, Cloud APIs from
over 430 million Knowledge Items
Find more libraries
Reuse Solution Kits and Libraries Curated by Popular Use Cases

Save this library and start creating your kit

Share this Page

share link
Find, review, and download reusable Libraries, Code Snippets, Cloud APIs from
over 430 million Knowledge Items
Find more libraries
Reuse Solution Kits and Libraries Curated by Popular Use Cases

Save this library and start creating your kit

  • © 2022 Open Weaver Inc.