Skip to content

[Help Wanted] feat: add cover flow to player#653

Draft
tvillega wants to merge 9 commits into
eddyizm:developmentfrom
tvillega:feat-add-cover-flow-to-player
Draft

[Help Wanted] feat: add cover flow to player#653
tvillega wants to merge 9 commits into
eddyizm:developmentfrom
tvillega:feat-add-cover-flow-to-player

Conversation

@tvillega
Copy link
Copy Markdown
Contributor

Addresses #454.

This is a UI implementation of Cover Flow, it hardcodes external URLs to showcase how it works.

UI showcase
recording_20260510_141954.mp4

The lyrics button acts as a swapper between the old ViewPager2 (page 0 is album cover, page 1 is lyrics view) and the new RecyclerView (dog pictures). What's particular about the later, aside from the nice visual re-dimension effect, is that it snaps in place on each horizontal scroll rather than drifting through the entire list at once.

What's left to do to complete the feature is to sync the RecyclerView elements with the current queue. Both lists must go synced and on each swap the currently playing song must change to next/prev and move forward/back the queue.

@tvillega
Copy link
Copy Markdown
Contributor Author

@eddyizm please add a help wanted flag to this PR.

I hope somebody can help attaching the queue to this new UI.

@eddyizm eddyizm added the help wanted Extra attention is needed label May 10, 2026
This is exactly the same as before, but with an MVVM architecture patter with the following flow:
Fragment -> RecyclerView -> Adapter -> ViewModel -> Repository -> Glide (data fetch).

Here comes the fun part. setupCoverFlow (ran in the fragment when the UI is built), must be fed
with the coverArt id recollected in the repository (all the way down on the MVVM chain).

I stopped here because I have a confusion on the Repository -> Glide MVVM chain. The image fetch
is done on the fragment, so the repository must read the queue itself to get the coverArt id?
@tvillega
Copy link
Copy Markdown
Contributor Author

tvillega commented May 21, 2026

With the new scaffolding, this is how CoverFlow is populated with images:

    // Fragment is built (player UI), then this is run
    public void onViewCreated(@NonNull View root, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(root, savedInstanceState);

        playerCoverFlow = root.findViewById(R.id.player_cover_flow);

        CoverRepository repository = new MediaBrowserCoverRepository(mediaBrowserListenableFuture);
        CoverFlowViewModelFactory factory = new CoverFlowViewModelFactory(repository);

        viewModel = new ViewModelProvider(this, factory)
                .get(CoverFlowViewModel.class);

        viewModel.getCovers().observe(getViewLifecycleOwner(), this::setupCoverFlow);
    }

    // Last line of the previous code hooks to this function to populate covers when data arrives
    private void setupCoverFlow(@NonNull List<Cover> covers) {
        if (covers.isEmpty()) return;

        CoverFlowAdapter adapter = new CoverFlowAdapter(
                requireContext(),
                covers,
                (cover, imageView) -> {
                    String coverArtId = cover.getCoverArtId();
                    if (coverArtId != null) {
                        CustomGlideRequest.Builder
                                .from(requireContext(),
                                        coverArtId,
                                        CustomGlideRequest.ResourceType.Song)
                                .build()
                                .into(imageView);
                    }
                });

        playerCoverFlow.setAdapter(adapter);
        playerCoverFlow.setLayoutManager(
                new LinearLayoutManager(requireContext(),
                        LinearLayoutManager.HORIZONTAL,
                        false));

        playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32));
        playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll());
        UIUtil.centerAndSnapRecyclerView(playerCoverFlow);
    }

I followed Android's MVVM architecture pattern (which other parts of this app already follow), summarized as:

Fragment → RecyclerView → Adapter → ViewModel → LiveData Observer → Repository → network call

The snippet above corresponds to Fragment → RecyclerView, from there we can skip to Repository → network call:

    public List<Cover> getCovers() throws Exception {
        MediaBrowser mediaBrowser = mediaBrowserFuture.get();
        MediaMetadata metadata = mediaBrowser.getMediaMetadata(); // <- Unused var

        List<String> urls = Arrays.asList(
                "https://images.dog.ceo/breeds/affenpinscher/n02110627_11858.jpg",
                "https://images.dog.ceo/breeds/hound-english/n02089973_811.jpg",
                "https://images.dog.ceo/breeds/shiba/shiba-14.jpg"
        );

        List<Cover> covers = new ArrayList<>();
        for (String url : urls) {
            covers.add(new Cover(url, null));
        }
        return covers;
    }

Those hardcoded URL's are the mockup that feed the recycler view.

The Cover item can contain both an URL (the current scenario) and a coverArtId (the alternative scenario). CoverFlowAdapter is in charge of fetching the image from the URL, which is the desired scenario.

If an URL is not provided and a coverArtId is provided instead, CustomGliderRequest in the fragment is capable to act as a fallback to populate the RecyclerView.

I don't know which one of them will be useful, but the one that stays will be sent to the Repository file.

What's next?

Find out how to extract either the coverArtId or the URL from the queue.


An alternative path I just discovered is to not create the MVVM arch model for CoverFlow and instead hook to the existent MVVM from PlayerQueueFragment. This would re-use code and keep them in sync by design.

I actually like more this approach, but I took the long path to understand how that even works in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

help wanted Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants