Code Example

Kotlin Android Screen Recording Snippets

A step by step Android Screen Recording example.

1. bmcreations/scrcast

Drop-in Android Screen Recording Library.

scrcast Example Tutorial

A fully, featured replacement for screen recording needs backed by Kotlin with the power of Coroutines and Android Jetpack. scrcast is:

  • Easy to use: scrcast's API leverages Kotlin languages features for simplicity, ease of use, and little-to-no boilerplate. Simply configure and record()
  • Modern: scrcast is Kotlin-first and uses modern libraries including Coroutines and Android Jetpack.

Download

scrcast is available on mavenCentral().

implementation ("dev.bmcreations:scrcast:$version")

Quick Start

scrcast provides a variety of configuration options for capturing, storing, and providing user interactions with your screen recordings.

Configuring

val recorder = ScrCast.use(activity)
recorder.apply {
    // configure options via DSL
    options {
        video {
            maxLengthSecs = 360
        }
        storage {
            directoryName = "scrcast-sample"
        }
        notification {
            title = "Super cool library"
            description = "shh session in progress"
            icon = resources.getDrawable(R.drawable.ic_camcorder, null).toBitmap()
            channel {
                id = "1337"
                name = "Recording Service"
            }
            showStop = true
            showPause = true
            showTimer = true
        }
        moveTaskToBack = false
        startDelayMs = 5_000
    }
}

You can find full configuration details and documentation here.

State

interaction with MediaRecorderis abstracted in a easy to use and manage interface, via explict state-changing accessors.

Start

recorder.record()

Stop

recorder.stopRecording()

Pause

recorder.pause()

Resume

recorder.resume()

Callbacks

State changes are emitted via RecordingCallbacks as a single interface or via a discrete lambda onRecordingStateChange
Completed recording output file is also emittable in RecordingCallbacks via

fun onRecordingFinished(file: File)

Requirements

  • AndroidX
  • minSdkVersion 23+
  • compileSdkVersion 28+
  • Java 8+
    Gradle (.gradle)
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Gradle Kotlin DSL (.gradle.kts)

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}
  • Full Example

Follow these steps to create a full example based on this tutorial:

Step 1. Design Layouts

We need to design our XML layouts as follows:

(a). activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:fitsSystemWindows="true">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Theme.App.AppBarOverlay">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:toolbarId="@+id/toolbar">

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/Theme.App.PopupOverlay" />

        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="?actionBarSize"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">

        <com.google.android.material.textview.MaterialTextView
            android:id="@+id/start_timer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="36sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_gravity="center"
            tools:text="5" />

        <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
            android:id="@+id/pause_fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="64dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:iconTint="@android:color/white"
            android:text="Pause"
            android:textColor="@android:color/white"
            app:icon="@drawable/ic_pause" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottom_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:fabAlignmentMode="end"
        app:fabAnimationMode="slide"
        app:fabCradleRoundedCornerRadius="10dp"
        />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        app:layout_anchor="@id/bottom_bar"
        app:tint="@android:color/white"
        app:srcCompat="@drawable/ic_camcorder" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Step 2. Write Code

Finally we need to write our code as follows:

(a). StateObserverActivity.kt

package dev.bmcreations.scrcast.app.list

import android.os.Bundle
import androidx.lifecycle.Observer
import dev.bmcreations.scrcast.lifecycle.observeRecordingState

class StateObserverActivity : MainActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        recorder.observeRecordingState(this, Observer { state -> handleRecorderState(state) })
    }
}

(b). StateCallbackActivity.kt

package dev.bmcreations.scrcast.app.list

class StateCallbackActivity : MainActivity() {

    override fun onResume() {
        super.onResume()
        recorder.onRecordingStateChange { state -> handleRecorderState(state) }
    }

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

(c). SimpleNotificationProvider.kt

package dev.bmcreations.scrcast.app.list

import android.app.Notification
import android.content.Context
import android.os.Build
import dev.bmcreations.scrcast.app.R
import dev.bmcreations.scrcast.recorder.RecordingState
import dev.bmcreations.scrcast.recorder.notification.NotificationProvider

class SimpleNotificationProvider(private val context: Context) : NotificationProvider(context) {

    init {
        createNotificationChannel()
    }

    override fun getChannelId(): String = CHANNEL_ID

    override fun get(state: RecordingState): Notification {
        val builder = with(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            Notification.Builder(context, CHANNEL_ID)
        } else {
            Notification.Builder(context)
        }) {
            setOngoing(true)
            setContentTitle("scrcast-sample")
            setContentText(when (state) {
                RecordingState.Recording -> "state=recording"
                is RecordingState.Idle -> "state=idle"
                RecordingState.Paused -> "state=paused"
                is RecordingState.Delay -> "state=delay"
            })

            setSmallIcon(R.drawable.ic_camcorder)
        }

        return builder.build()
    }

    override fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = android.app.NotificationChannel(
                CHANNEL_ID,
                CHANNEL_NAME,
                android.app.NotificationManager.IMPORTANCE_HIGH
            ).apply {
                lightColor = android.graphics.Color.BLUE
                lockscreenVisibility = Notification.VISIBILITY_PUBLIC
            }

            notificationManager.createNotificationChannel(notificationChannel)
        }
    }

    override fun getNotificationId(): Int = 2000

    companion object {
        private const val CHANNEL_ID = "1338"
        private const val CHANNEL_NAME = "Recording Service Provided"
    }
}

(d). JavaMainActivity.java

package dev.bmcreations.scrcast.app.list.jvm;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Observer;

import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.textview.MaterialTextView;

import org.jetbrains.annotations.NotNull;

import java.io.File;

import dev.bmcreations.scrcast.ScrCast;
import dev.bmcreations.scrcast.app.R;
import dev.bmcreations.scrcast.app.list.FABExtensions;
import dev.bmcreations.scrcast.config.ChannelConfig;
import dev.bmcreations.scrcast.config.Options;
import dev.bmcreations.scrcast.config.StorageConfig;
import dev.bmcreations.scrcast.config.VideoConfig;
import dev.bmcreations.scrcast.internal.config.dsl.NotificationConfigBuilder;
import dev.bmcreations.scrcast.lifecycle.ScrCastLifecycleObserver;
import dev.bmcreations.scrcast.recorder.RecordingCallbacks;
import dev.bmcreations.scrcast.recorder.RecordingState;

public class JavaMainActivity extends AppCompatActivity {

    private FloatingActionButton fab;
    private MaterialTextView startTimer;
    private ScrCast recorder;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fab = findViewById(R.id.fab);
        startTimer = findViewById(R.id.start_timer);

        setupRecorder();

        bindViews();
    }

    private void setupRecorder() {
        recorder = ScrCast.use(this);

        // create configuration for video
        VideoConfig videoConfig = new VideoConfig(
                -1,
                -1,
                MediaRecorder.VideoEncoder.H264,
                8_000_000,
                360
        );

        // create configuration for storage
        StorageConfig storageConfig = new StorageConfig("scrcast-sample");

        // create configuration for notification channel for recording
        ChannelConfig channelConfig = new ChannelConfig("1337", "Recording Service");

        // create configuration for our notification
        Drawable icon = ContextCompat.getDrawable(this, R.drawable.ic_camcorder);
        NotificationConfigBuilder  notificationConfig = new NotificationConfigBuilder();
        notificationConfig.setShowPause(true);
        notificationConfig.setIcon(drawableToBitmap(icon));
        notificationConfig.setShowStop(true);
        notificationConfig.setShowTimer(true);
        notificationConfig.setChannel(channelConfig);

        Options options = new Options(
                videoConfig,
                storageConfig,
                notificationConfig.build(),
                false,
                5000,
                true
        );

        // set our options
        recorder.updateOptions(options);

        // listen for state changes
        recorder.setRecordingCallback(new RecordingCallbacks() {
            @Override
            public void onStateChange(@NotNull RecordingState state) {
                FABExtensions.reflectRecorderState(fab, state);
                startTimer.setVisibility(state instanceof RecordingState.Delay ? View.VISIBLE : View.GONE);
                if (state instanceof RecordingState.Delay) {
                    startTimer.setText(((RecordingState.Delay) state).getRemainingSeconds());
                }
            }

            @Override
            public void onRecordingFinished(@NotNull File file) {
                Toast.makeText(
                        JavaMainActivity.this,
                        "result file is located at " + file.getAbsolutePath(),
                        Toast.LENGTH_SHORT
                ).show();
            }
        });
    }

    private void bindViews() {
        fab.setOnClickListener(v -> {
            if (recorder.getState().isRecording()) {
                recorder.stopRecording();
            } else {
                recorder.record();
            }
        });
    }

    public static Bitmap drawableToBitmap (Drawable drawable) {
        Bitmap bitmap;

        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            if(bitmapDrawable.getBitmap() != null) {
                return bitmapDrawable.getBitmap();
            }
        }

        if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
        } else {
            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }
}

(e). FABExtensions.kt

package dev.bmcreations.scrcast.app.list

import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import dev.bmcreations.scrcast.app.R
import dev.bmcreations.scrcast.recorder.RecordingState
import dev.bmcreations.scrcast.recorder.RecordingState.Idle
import dev.bmcreations.scrcast.recorder.RecordingState.Recording

@SuppressLint("ObjectAnimatorBinding")
private fun FloatingActionButton.animateColorChange(@ColorInt fromColor: Int, @ColorInt toColor: Int, startDelay: Long = 0) {
    val colorAnimator = ObjectAnimator.ofArgb(
        this,
        "backgroundTintColor",
        fromColor, toColor
    ).apply {
        setStartDelay(startDelay)
        interpolator = AccelerateDecelerateInterpolator()
        addUpdateListener { animation ->
            val animatedValue = animation.animatedValue as Int
            backgroundTintList = ColorStateList.valueOf(animatedValue)
        }
    }

    colorAnimator.start()
}

fun FloatingActionButton.reflectState(state: RecordingState) {
    if (state == Recording || state is Idle) {
        val isRecording = state == Recording
        setImageResource(if (isRecording) R.drawable.ic_stop else R.drawable.ic_camcorder)
        animateColorChange(
            if (isRecording) ContextCompat.getColor(
                context,
                R.color.teal200
            ) else ContextCompat.getColor(context, R.color.stop_recording),
            if (isRecording) ContextCompat.getColor(
                context,
                R.color.stop_recording
            ) else ContextCompat.getColor(context, R.color.teal200)
        )
    }
}

class FABExtensions {
    companion object {
        @JvmStatic
        fun reflectRecorderState(fab: FloatingActionButton, state: RecordingState) {
            fab.reflectState(state)
        }
    }
}

(f). MainActivity.kt

package dev.bmcreations.scrcast.app.list

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
import dev.bmcreations.scrcast.ScrCast
import dev.bmcreations.scrcast.app.R
import dev.bmcreations.scrcast.extensions.supportsPauseResume
import dev.bmcreations.scrcast.recorder.RecordingState
import kotlinx.android.synthetic.main.activity_main.*

abstract class MainActivity : AppCompatActivity() {

    protected val recorder: ScrCast by lazy {
        ScrCast.use(this).apply {
            options {
                video {
                    maxLengthSecs = 360
                }
                storage {
                    directoryName = "scrcast-sample"
                }
                notification {
                    icon = resources.getDrawable(R.drawable.ic_camcorder, null).toBitmap()
                    channel {
                        id = "1337"
                        name = "Recording Service"
                    }
                    showStop = true
                    showPause = true
                    showTimer = true
                }
                moveTaskToBack = false
                stopOnScreenOff = true
                startDelayMs = 5_000
            }

            // ScrCast supports running your own notifications completely
            // simply provide a NotificationProvider
            //
            //setNotificationProvider(SimpleNotificationProvider([email protected]))
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        pause_fab.hide()
        pause_fab.setOnClickListener {
            if (recorder.state.isPaused) {
                recorder.resume()
            } else {
                recorder.pause()
            }
        }

        fab.setOnClickListener {
            if (recorder.state.isRecording) {
                recorder.stopRecording()
            } else {
                recorder.record()
            }
        }
    }

    override fun onResume() {
        super.onResume()
        recorder.onRecordingComplete { file ->
            Snackbar.make(bottom_bar, "Recording located at ${file.absolutePath}", Snackbar.LENGTH_SHORT).show()
        }
    }

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

    protected fun handleRecorderState(state: RecordingState) {
        Log.d("sample", "state change: state = $state")
        fab.reflectState(state)

        start_timer.isVisible = state.isInStartDelay
        if (supportsPauseResume) {
            if (state.isRecording || state.isPaused) {
                pause_fab.show()
            } else {
                pause_fab.hide()
            }
        }

        when (state) {
            is RecordingState.Delay -> start_timer.text = state.remainingSeconds.toString()
            RecordingState.Recording -> {
                pause_fab.setIconResource(R.drawable.ic_pause)
                pause_fab.text = "Pause"
            }
            is RecordingState.Idle -> {
                fab.isExpanded = false
                if (state.error != null) {
                    Toast.makeText(this, "Unable to start recording", Toast.LENGTH_SHORT).show()
                }
            }
            RecordingState.Paused -> {
                pause_fab.setIconResource(R.drawable.ic_resume)
                pause_fab.text = "Resume"
            }
        }
    }
}

Reference

You can DOWNLOAD FULL CODE.
You can also browse code or read more here.
Follow code author here.


Read More.

Related Posts