본문 바로가기
안드로이드 자바

[Android][JAVA] RecyclerView Decoration으로 Indicator 구현하기

by teamnova 2023. 2. 18.
728x90

안녕하세요. 이번 시간에는 리사이클러뷰에 Decoration으로 Incator 구현을 해보겠습니다.

 

먼저 RecyclerView 의 추상클래스인 ItamDecoration 을 상속받은 클래스를 다음과 같이 구현합니다. 

 

LinePagerIndicatorDecoration.java

package com.example.recyclerviewevent.Event;

import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;

import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

public class LinePagerIndicatorDecoration extends RecyclerView.ItemDecoration{
    private int colorActive = 0xFFFFFFFF;
    private int colorInactive = 0x66FFFFFF;

    private static final float DP = Resources.getSystem().getDisplayMetrics().density;

    /**
     * Height of the space the indicator takes up at the bottom of the view.
     * 뷰 맨 아래에서 표시기가 차지하는 공간의 높이입니다
     */
    private final int mIndicatorHeight = (int) (DP * 16);

    /**
     * Indicator stroke width.
     */
    private final float mIndicatorStrokeWidth = DP * 1/2;

    /**
     * Indicator width.
     */
    private final float mIndicatorItemLength = DP * 16;
    /**
     * Padding between indicators.
     */
    private final float mIndicatorItemPadding = DP * 4;

    /**
     * Some more natural animation interpolation
     */
    private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();

    private final Paint mPaint = new Paint();

    public LinePagerIndicatorDecoration() {
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(mIndicatorStrokeWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

        int itemCount = parent.getAdapter().getItemCount();

        // center horizontally, calculate width and subtract half from center 가로로 중심을 잡고, 너비를 계산하고 중심에서 반을 뺍니다.
        float totalLength = mIndicatorItemLength * itemCount;
        float paddingBetweenItems = Math.max(0, itemCount - 1) * mIndicatorItemPadding;
        float indicatorTotalWidth = totalLength + paddingBetweenItems;
        float indicatorStartX = (parent.getWidth() - indicatorTotalWidth) / 2F;

        // center vertically in the allotted space 할당된 공간에 수직으로 중심을 잡다
        float indicatorPosY = parent.getHeight() - mIndicatorHeight / 2F;

        drawInactiveIndicators(c, indicatorStartX, indicatorPosY, itemCount);


        // find active page (which should be highlighted) 활성 페이지 찾기(강조되어야 함) 활성 페이지 찾기(강조되어야 함)
        LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
        int activePosition = layoutManager.findFirstVisibleItemPosition();
        if (activePosition == RecyclerView.NO_POSITION) {
            return;
        }

        // find offset of active page (if the user is scrolling) 활성 페이지의 오프셋 찾기(사용자가 스크롤 중인 경우)
        final View activeChild = layoutManager.findViewByPosition(activePosition);
        int left = activeChild.getLeft();
        int width = activeChild.getWidth();

        // on swipe the active item will be positioned from [-width, 0] 스와이프 시 활성 항목은 [-width, 0]에서 위치하게 됩니다.
        // interpolate offset for smooth animation 부드러운 애니메이션을 위해 간격띄우기 보간
        float progress = mInterpolator.getInterpolation(left * -1 / (float) width);

        drawHighlights(c, indicatorStartX, indicatorPosY, activePosition, progress, itemCount);
    }

    private void drawInactiveIndicators(Canvas c, float indicatorStartX, float indicatorPosY, int itemCount) {
        mPaint.setColor(colorInactive);

        // width of item indicator including padding 패딩을 포함한 indicator 폭
        final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

        float start = indicatorStartX;
        for (int i = 0; i < itemCount; i++) {
            // draw the line for every item
            c.drawLine(start, indicatorPosY, start + mIndicatorItemLength, indicatorPosY, mPaint);
            start += itemWidth;
        }
    }

    private void drawHighlights(Canvas c, float indicatorStartX, float indicatorPosY,
                                int highlightPosition, float progress, int itemCount) {
        mPaint.setColor(colorActive);

        // width of item indicator including padding
        final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

        if (progress == 0F) {
            // no swipe, draw a normal indicator
            float highlightStart = indicatorStartX + itemWidth * highlightPosition;
            c.drawLine(highlightStart, indicatorPosY,
                    highlightStart + mIndicatorItemLength, indicatorPosY, mPaint);
        } else {
            float highlightStart = indicatorStartX + itemWidth * highlightPosition;
            // calculate partial highlight
            float partialLength = mIndicatorItemLength * progress;

            // draw the cut off highlight
            c.drawLine(highlightStart + partialLength, indicatorPosY,
                    highlightStart + mIndicatorItemLength, indicatorPosY, mPaint);

            // draw the highlight overlapping to the next item as well
            if (highlightPosition < itemCount - 1) {
                highlightStart += itemWidth;
                c.drawLine(highlightStart, indicatorPosY,
                        highlightStart + partialLength, indicatorPosY, mPaint);
            }
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom = mIndicatorHeight;
    }
}

 

그 다음 리사이클러뷰에 필요한 데이터클래스와 어댑터를 정의합니다.

 

Photo.java

package com.example.recyclerviewevent;

public class Photo {
    private int path;

    public Photo(int path) {
        this.path = path;
    }

    public int getPath() {
        return path;
    }

    public void setPath(int path) {
        this.path = path;
    }

    @Override
    public String toString() {
        return "Photo{" +
                "path=" + path +
                '}';
    }
}

GalleyAdapter.java

package com.example.recyclerviewevent;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;

public class GalleyAdapter extends RecyclerView.Adapter<GalleyAdapter.ViewHolder>{

    private ArrayList<Photo> uriList  ;

    public GalleyAdapter(ArrayList<Photo> uriList) {
        this.uriList = uriList;
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        private ImageView image;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            this.image=itemView.findViewById(R.id.image);
        }
    }


    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.image_item, parent, false);
        ViewHolder vh=new ViewHolder(view);
        return vh;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.image.setImageResource(uriList.get(position).getPath());
    }

    @Override
    public int getItemCount() {
        return uriList.size();
    }
}

 

MainActivity.java

package com.example.recyclerviewevent;


import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.PagerSnapHelper;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;

import com.example.recyclerviewevent.Event.LinePagerIndicatorDecoration;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private String TAG=this.getClass().getSimpleName();
    private GalleyAdapter adapter;
    private LinearLayoutManager layoutManager;
    private ArrayList<Photo> photoList=new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView=findViewById(R.id.rv);
        initRecyclerView();
    }
    //Dummy Data
    void setData(){
        photoList.add(new Photo(R.drawable.dog1));
        photoList.add(new Photo(R.drawable.dog2));
        photoList.add(new Photo(R.drawable.dog3));
        photoList.add(new Photo(R.drawable.dog4));
    }
    //리사이클러뷰 초기화
    private void initRecyclerView() {
        setData();
        layoutManager=new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false);
        recyclerView.setLayoutManager(layoutManager);
        adapter=new GalleyAdapter(photoList);
        recyclerView.setAdapter(adapter);
        setRecyclerViewEvent();
    }
    //리사이클러뷰 이벤트 설정
    private void setRecyclerViewEvent() {
        /**
         * Implementation of the SnapHelper supporting pager style snapping in either vertical or horizontal orientation.
         * PagerSnapHelper can help achieve a similar behavior to androidx.viewpager.widget.ViewPager. Set both RecyclerView and the items of the RecyclerView.Adapter to have android.view.ViewGroup.LayoutParams.MATCH_PARENT height and width and then attach PagerSnapHelper to the RecyclerView using attachToRecyclerView(RecyclerView).*/
        PagerSnapHelper snapHelper = new PagerSnapHelper();
        /**
         * Add an RecyclerView.ItemDecoration to this RecyclerView. Item decorations can affect both measurement and drawing of individual item views.
         * Item decorations are ordered. Decorations placed earlier in the list will be run/queried/drawn first for their effects on item views. Padding added to views will be nested; a padding added by an earlier decoration will mean further item decorations in the list will be asked to draw/pad within the previous decoration's given area.
         * Params:
         * decor – Decoration to add
         * */
        recyclerView.addItemDecoration(new LinePagerIndicatorDecoration());
        /**
         * Attaches the SnapHelper to the provided RecyclerView, by calling RecyclerView.setOnFlingListener(RecyclerView.OnFlingListener). You can call this method with null to detach it from the current RecyclerView.
         * Params:
         * recyclerView – The RecyclerView instance to which you want to add this helper or null if you want to remove SnapHelper from the current RecyclerView.
         * Throws:
         * IllegalArgumentException – if there is already a RecyclerView.OnFlingListener attached to the provided RecyclerView.*/
        snapHelper.attachToRecyclerView(recyclerView);
    }
}

activity_main.xml

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.recyclerview.widget.RecyclerView
            android:background="#E91E63"
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginStart="5dp"
            android:layout_marginTop="200dp"
            android:layout_marginEnd="5dp"
            android:layout_marginBottom="200dp" />
    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

image_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    >
    <ImageView
        android:id="@+id/image"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:scaleType="fitXY"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</RelativeLayout>