Creating a timeline-based video list in Android requires combining RecyclerView with custom decorations to achieve a vertical chronological layout. This approach is commonly used in video editing applications, video galleries, and surveillance monitoring apps where time progression is visually important.
Core Implementation Components
To build a timeline video list, you need to address these key areas:
- Custom Layout Design: Create list items that accommodate video thumbnails, metadata (title, duration, timestamp), and timeline markers.
- Data Model: Define a model class holding video properties such as source URL, display name, duration, and timestamp.
- RecyclerView Adapter: Implement an adapter to bind video data to the list items.
- ItemDecoration: Draw the timeline axis and connecting lines between items.
- Thumbnail Loading: Use libraries like Glide or ExoPlayer to load video previews efficiently.
Adding Required Dependencies
Include the RecyclerView dependency in your module's build.gradle file:
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.github.bumptech.glide:glide:4.16.0'
}
Designing the List Item Layout
Create res/layout/item_video_timeline.xml with a horizontal arrangement containing the timeline marker, thumbnail, and text information:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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="wrap_content"
android:padding="12dp">
<!-- Timeline node -->
<View
android:id="@+id/timelineNode"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/circle_accent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp"/>
<!-- Video thumbnail -->
<ImageView
android:id="@+id/imgThumbnail"
android:layout_width="140dp"
android:layout_height="100dp"
android:layout_marginStart="16dp"
android:scaleType="centerCrop"
android:contentDescription="@string/video_thumbnail"
app:layout_constraintStart_toEndOf="@id/timelineNode"
app:layout_constraintTop_toTopOf="parent"/>
<!-- Video title -->
<TextView
android:id="@+id/txtVideoTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textSize="16sp"
android:textStyle="bold"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@id/imgThumbnail"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/imgThumbnail"/>
<!-- Duration label -->
<TextView
android:id="@+id/txtDuration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="#757575"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="@id/txtVideoTitle"
app:layout_constraintTop_toBottomOf="@id/txtVideoTitle"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Creating the Data Model
Define a model class to represent each video entry:
public class VideoEntry {
private String videoId;
private String sourcePath;
private String displayName;
private long durationMillis;
private long recordedAt;
public VideoEntry(String videoId, String sourcePath, String displayName,
long durationMillis, long recordedAt) {
this.videoId = videoId;
this.sourcePath = sourcePath;
this.displayName = displayName;
this.durationMillis = durationMillis;
this.recordedAt = recordedAt;
}
// Getters
public String getVideoId() { return videoId; }
public String getSourcePath() { return sourcePath; }
public String getDisplayName() { return displayName; }
public long getDurationMillis() { return durationMillis; }
public long getRecordedAt() { return recordedAt; }
}
Implementing the RecyclerView Adapter
Create an adapter that efficiently binds video data to the views:
public class TimelineVideoAdapter extends RecyclerView.Adapter<TimelineVideoAdapter.VideoHolder> {
private final List<VideoEntry> videoEntries;
private final OnVideoClickListener clickListener;
public interface OnVideoClickListener {
void onVideoClicked(VideoEntry entry, int position);
}
public TimelineVideoAdapter(List<VideoEntry> entries, OnVideoClickListener listener) {
this.videoEntries = entries != null ? entries : new ArrayList<>();
this.clickListener = listener;
}
@NonNull
@Override
public VideoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.item_video_timeline, parent, false);
return new VideoHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull VideoHolder holder, int position) {
VideoEntry entry = videoEntries.get(position);
holder.bindData(entry, position);
}
@Override
public int getItemCount() {
return videoEntries.size();
}
class VideoHolder extends RecyclerView.ViewHolder {
private final ImageView imgThumbnail;
private final TextView txtVideoTitle;
private final TextView txtDuration;
VideoHolder(@NonNull View itemView) {
super(itemView);
imgThumbnail = itemView.findViewById(R.id.imgThumbnail);
txtVideoTitle = itemView.findViewById(R.id.txtVideoTitle);
txtDuration = itemView.findViewById(R.id.txtDuration);
}
void bindData(VideoEntry entry, int position) {
txtVideoTitle.setText(entry.getDisplayName());
txtDuration.setText(formatPlaybackTime(entry.getDurationMillis()));
// Load thumbnail with Glide
Glide.with(itemView.getContext())
.load(entry.getSourcePath())
.placeholder(R.drawable.ic_video_placeholder)
.centerCrop()
.into(imgThumbnail);
itemView.setOnClickListener(v -> {
if (clickListener != null) {
clickListener.onVideoClicked(entry, position);
}
});
}
private String formatPlaybackTime(long millis) {
int seconds = (int) (millis / 1000);
int minutes = seconds / 60;
seconds = seconds % 60;
return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds);
}
}
}
Creating the Timeline Decoration
Use ItemDecoration to draw the vertical timeline connecting all items:
public class TimelineItemDecoration extends RecyclerView.ItemDecoration {
private final Paint linePaint;
private final int nodeRadius;
private final int timelineOffset;
public TimelineItemDecoration(Context context) {
nodeRadius = (int) (6 * context.getResources().getDisplayMetrics().density);
timelineOffset = (int) (18 * context.getResources().getDisplayMetrics().density);
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint.setColor(Color.parseColor("#1976D2"));
linePaint.setStrokeWidth(4f);
linePaint.setStyle(Paint.Style.STROKE);
}
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
super.onDraw(canvas, parent, state);
int childCount = parent.getChildCount();
if (childCount == 0) return;
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
int left = timelineOffset;
int top = child.getTop();
int bottom = child.getBottom();
// Draw vertical line
canvas.drawLine(left, top, left, bottom, linePaint);
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
outRect.set(timelineOffset * 2, 0, 0, 0);
}
}
Setting Up the RecyclerView
Configure the RecyclerView in your Activity or Fragment:
public class VideoTimelineActivity extends AppCompatActivity implements
TimelineVideoAdapter.OnVideoClickListener {
private RecyclerView recyclerView;
private TimelineVideoAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_timeline);
recyclerView = findViewById(R.id.recyclerViewTimeline);
setupRecyclerView();
loadVideoData();
}
private void setupRecyclerView() {
adapter = new TimelineVideoAdapter(new ArrayList<>(), this);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
recyclerView.addItemDecoration(new TimelineItemDecoration(this));
}
private void loadVideoData() {
List<VideoEntry> sampleVideos = generateSampleData();
adapter = new TimelineVideoAdapter(sampleVideos, this);
recyclerView.setAdapter(adapter);
}
private List<VideoEntry> generateSampleData() {
List<VideoEntry> entries = new ArrayList<>();
// Add sample video entries
entries.add(new VideoEntry("1", "/path/video1.mp4", "Morning Walk", 125000,
System.currentTimeMillis()));
entries.add(new VideoEntry("2", "/path/video2.mp4", "Meeting Recording", 360000,
System.currentTimeMillis()));
return entries;
}
@Override
public void onVideoClicked(VideoEntry entry, int position) {
// Handle video click - open detail or play video
Intent intent = new Intent(this, VideoDetailActivity.class);
intent.putExtra("VIDEO_PATH", entry.getSourcePath());
startActivity(intent);
}
}
For advanced scenarios, consider adding video preview functionality using ExoPlayer to display frame-by-frame previews when users long-press on timeline items.