rubensousa / DpadRecyclerView

A RecyclerView built for Android TV with Compose in mind and as a replacement for Leanback's BaseGridView.
https://rubensousa.github.io/DpadRecyclerView/
Apache License 2.0
136 stars 17 forks source link

Did you find a solution for the Grid focus problem? #164

Closed mohamedriadhzrig closed 1 year ago

mohamedriadhzrig commented 1 year ago

Everytime I update the DpadRecycleView it goes messy,and nothing is shown in the Logcat Thank you in advance for your help

rubensousa commented 1 year ago

Hello @mohamedriadhzrig. You need to share more information, otherwise I can't help. What issue do you have exactly?

mohamedriadhzrig commented 1 year ago

Heyy,let make it easy to understand, I'm using 2 RV in an activity,Both of them have a vertical oriontation first one is linear and the second one is a grid with 5 span the click on an item of the first RV update the items of the second Rv(grid one) when I navigate in the Grid one and go back to the first RV and click to update the Grid The grid go messy,it's like all of them are going to the first position and paging source don't stop of requestiong the server But when I click on items of the first rv without going to the secong RV and navigate it work fine(Grid),it update on every click without any problem so I assumed that the problem is comming from the focus changing when I navigate in the grid RV

rubensousa commented 1 year ago

@mohamedriadhzrig can you please upload a sample project that reproduces that issue? Are you sure that's a problem with DpadRecyclerView and not a bug somewhere else?

so I assumed that the problem is comming from the focus changing when I navigate in the grid RV

Focus changes shouldn't "mess" the grid unless you trigger adapter updates on item selection. Again, please upload a sample that showcases the problem so I can have a look

mohamedriadhzrig commented 1 year ago
    rv is the grid RevycleView

private void setupMoviesRecyclerView() { moviesAdapter = new MoviesAdapter(new MovieComparator(), requestManager); mainActivityViewModel = new ViewModelProvider(this).get(MovieViewModel.class); initRecyclerviewAndAdapter(); }

private void initRecyclerviewAndAdapter() { rv = findViewById(R.id.rv); rv.setOrientation(RecyclerView.VERTICAL); if (Callback.isTvBox) rv.setFadingEdgeLength(110); rv.setSmoothFocusChangesEnabled(false); rv.setHasFixedSize(true); rv.setAdapter(moviesAdapter.withLoadStateFooter(new LoadStateAdapter(v -> moviesAdapter.retry()))); }

private void recreate_data(int position) { mainActivityViewModel.moviePagingDataFlowable.subscribe(moviePagingData -> moviesAdapter.submitData(getLifecycle(), moviePagingData)); }

public void setAdapterToCatListview() { adapter_category = new AdapterCategory(this, arrayListCat, (item, position) -> new Handler().postDelayed(() -> recreate_data(position), 0)); rv_cat.setAdapter(adapter_category); adapter_category.select(1); pos = 1; }

private void setupCategory() { arrayListCat = new ArrayList<>(); rv_cat.setItemAnimator(new DefaultItemAnimator()); rv_cat.setHasFixedSize(true); getDataCat(); }

the only solution that make the Recycleview rv work fine is to use this before updating its data

rv.setSelectedPosition(0);

rubensousa commented 1 year ago

That code snippet has a couple of issues:

  1. Do not call setItemAnimator, specially if you're using the default item animator. DpadRecyclerView doesn't like custom animators that have changes enabled. Please remove that line.

  2. Looks like you're subscribing to the moviePagingDataFlowable multiple times. Please keep only one subscription active.

  3. I don't see a usage of OnViewHolderSelectedListener in the snippet above. How are you observing item selections? Please have a look here: https://rubensousa.github.io/DpadRecyclerView/selection/

mohamedriadhzrig commented 1 year ago

hey again,I fixed it by updating this line rv.setAdapter(moviesAdapter.withLoadStateFooter(new LoadStateAdapter(v -> moviesAdapter.retry()))); to rv.setAdapter(moviesAdapter);

but when I update the recycleview(rv) the selection of the item jump to the last item of the new loaded item(14 per page) so I have to add this rv.setSelectedPosition(0); to avoid hiding the 2 row (even with that sometimes it jump to the last)

I'll share with you the activity and the adapter of rv

@AndroidEntryPoint public class MovieActivity extends AppCompatActivity { private CategoryViewModel categoryViewModel; private MovieViewModel movieViewModel; private CategoryAdapter categoryAdapter; private MoviesAdapter moviesAdapter; private DpadRecyclerView rv_cat, rv;

@Inject
RequestManager requestManager;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setupOrientation();
    initViews();
    setupCategoryRecyclerView();
    setupMoviesRecyclerView();
}

private void setupOrientation() {
    if (Callback.isLandscape) setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    IsStatusBar.ifSupported(this);
    IsScreenshot.ifSupported(this);
    IsRTL.ifSupported(this);
}

private void initViews() {
    setContentView(R.layout.activity_live_tv);
    findViewById(R.id.theme_bg).setBackgroundResource(R.drawable.bg_dark_panther);
    findViewById(R.id.iv_back_page).setOnClickListener(view -> onBackPressed());

    rv_cat = findViewById(R.id.rv_cat);
    rv_cat.setSelectedPosition(1);
    rv_cat.setHasFixedSize(true);

    TextView page_title = findViewById(R.id.tv_page_title);
    page_title.setText(getString(R.string.movies_home));
}

private void setupCategoryRecyclerView() {
    categoryAdapter = new CategoryAdapter(this, new ArrayList<>(), (item, position) -> {
        categoryAdapter.select(position);
        movieViewModel.setCategoryId(Callback.categoryId);
        rv.setSelectedPosition(0); 
        //because when I select a category, the selection jump to the last item of the first loaded page (14/page)
    });

    rv_cat.setAdapter(categoryAdapter);

    categoryViewModel = new ViewModelProvider(this, new CategoryViewModelFactory(
            new CategoryRepository(
                    MyApplication.getInstance().getAppDatabase().categoryDao(),
                    MyApplication.getInstance().getApiService()
            )
    )).get(CategoryViewModel.class);

    categoryViewModel.getAllCategoriesFromCache(CategoryType.MOVIE).observe(this, categories -> {
        Callback.categoryId = categories.get(0).getId();
        categories.add(0, new Category("", getString(R.string.favourite), getString(R.string.favourite), CategoryType.MOVIE));
        categoryAdapter.setCategories(categories);
        rv_cat.setSelectedPosition(1);
        observeMoviesData();
    });
}

private void observeMoviesData() {
    movieViewModel.getMovies().observe(this, pagingData -> moviesAdapter.submitData(getLifecycle(), pagingData));
    movieViewModel.setCategoryId(Callback.categoryId);
}

private void setupMoviesRecyclerView() {
    moviesAdapter = new MoviesAdapter(new MovieComparator(), requestManager);
    movieViewModel = new ViewModelProvider(this).get(MovieViewModel.class);
    initializeRecyclerView();
}

private void initializeRecyclerView() {
    rv = findViewById(R.id.rv);
    rv.setOrientation(RecyclerView.VERTICAL);
    if (Callback.isTvBox) rv.setFadingEdgeLength(110);
    rv.setSmoothFocusChangesEnabled(false);
    rv.setHasFixedSize(true);
    rv.setSpanCount(5);

// rv.setAdapter(moviesAdapter.withLoadStateFooter(new LoadStateAdapter(v -> moviesAdapter.retry()))); adding footer is // making the problem rv.setAdapter(moviesAdapter); }

private long lastDPadDownEventTime = 0;

@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
    if (event.getAction() == KeyEvent.ACTION_DOWN && rv.hasFocus()) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_DOWN:
                if (System.currentTimeMillis() - lastDPadDownEventTime < 345) return true;
                lastDPadDownEventTime = System.currentTimeMillis();
                break;
            case KeyEvent.KEYCODE_BACK:
                onBackPressed();
                return true;
        }
    }
    return super.onKeyDown(keyCode, event);
}

@Override
public void onBackPressed() {
    super.onBackPressed();
}

@Override
public void onDestroy() {
    super.onDestroy();
}

}

public class MoviesAdapter extends PagingDataAdapter<Movie, MoviesAdapter.MovieViewHolder> { private static final int LOADING_ITEM = 0; private static final int MOVIE_ITEM = 1;

private final int columnWidth;
private final int columnHeight;
private final RequestManager glide;

public MoviesAdapter(@NonNull DiffUtil.ItemCallback<Movie> diffCallback, RequestManager glide) {
    super(diffCallback);
    this.glide = glide;
    this.columnWidth = Callback.columnWidth;
    this.columnHeight = (int) (columnWidth * 1.15);
}

@NonNull
@Override
public MovieViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new MovieViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_movie, parent, false));
}

@Override
public void onBindViewHolder(@NonNull MovieViewHolder holder, int position) {
    Movie movie = getItem(position);
    if (movie != null) {
        configureViewHolder(holder, movie);
    }
}

private void configureViewHolder(MovieViewHolder holder, Movie movie) {
    holder.tv_movie_title.setText(movie.getName());
    setMovieRating(holder, movie);
    setImage(holder, movie);

    holder.fd_movie_card.setOnFocusChangeListener((v, hasFocus) -> {
        float scale = hasFocus ? 1.1f : 1.0f;
        float alpha = hasFocus ? 1.0f : 0.5f;
        float elevation = hasFocus ? 50f : 0f;

        holder.itemView.animate()
                .setInterpolator(new AccelerateDecelerateInterpolator())
                .scaleX(scale)
                .scaleY(scale)
                .setDuration(500)
                .start();

        holder.itemView.setAlpha(alpha);
        ViewCompat.setElevation(holder.itemView, elevation);
    });

    holder.fd_movie_card.setOnClickListener(view -> {
        animateClick(view);
        Intent intent = new Intent(view.getContext(), DetailsMovieActivity.class);
        intent.putExtra("movie_object", movie);
        view.getContext().startActivity(intent);
    });
}

private void setImage(MovieViewHolder holder, Movie movie) {
    try {
        glide.load(movie.getStreamIcon().isEmpty() ? "null" : movie.getStreamIcon())
                .thumbnail(glide.load(R.mipmap.ic_launcher))
                .into(holder.iv_movie);
    } catch (Exception e) {
        glide.load(R.mipmap.ic_launcher).into(holder.iv_movie);
    }
    holder.iv_movie.setScaleType(ImageView.ScaleType.CENTER_CROP);
    holder.iv_movie.setLayoutParams(new RelativeLayout.LayoutParams(columnWidth, columnHeight));
}

private void setMovieRating(MovieViewHolder holder, Movie movie) {
    if (movie.getRating().isEmpty()) {
        holder.tv_movie_rating.setVisibility(View.INVISIBLE);
    } else {
        String ratingText = movie.getRating().length() > 2 ? movie.getRating().substring(0, 3) : movie.getRating();
        holder.tv_movie_rating.setText(ratingText);
        holder.tv_movie_rating.setVisibility(View.VISIBLE);
    }
}

private void animateClick(View view) {
    view.animate().scaleX(1.1f).scaleY(1.1f).setDuration(100)
            .withEndAction(() -> view.animate().scaleX(1f).scaleY(1f).setDuration(100).start())
            .start();
}

@Override
public int getItemViewType(int position) {
    return position == getItemCount() ? MOVIE_ITEM : LOADING_ITEM;
}

public static class MovieViewHolder extends RecyclerView.ViewHolder {
    SimpleRoundedImageView iv_movie;
    TextView tv_movie_title, tv_movie_rating;
    View fd_movie_card;

    public MovieViewHolder(@NonNull View itemView) {
        super(itemView);
        iv_movie = itemView.findViewById(R.id.iv_movie);
        tv_movie_title = itemView.findViewById(R.id.tv_movie_title);
        tv_movie_rating = itemView.findViewById(R.id.tv_movie_rating);
        fd_movie_card = itemView.findViewById(R.id.fd_movie_card);
    }
}

}

rubensousa commented 1 year ago

Please upload this sample to a github repository that reproduces the issue you're describing. Alternatively, fork this repo and update its sample to showcase the issue. This really seems like an issue of misusing the adapter update mechanisms and triggering unnecessary selections.

rubensousa commented 1 year ago

@mohamedriadhzrig any update here?

mohamedriadhzrig commented 1 year ago

@rubensousa I cannot share the code ,I already got in trouble sharing that activity and adapter...Anyway,I kind of find a solution,I'm not 100% convinced but I don't have choices,I have to scroll to a negative position before updating the list of the adapter

rubensousa commented 1 year ago

You shouldn't need to scroll to a negative position, and I'm not sure that the selection will be applied in that case. I'll close this issue since this seems like an user error. If you can find time to create a proper sample, feel free to comment.