1. Infinite Scroll이란

Infinite Scroll은 무한스크롤, 즉 유튜브나 페이스북, 트위터 등에서 스크롤이 끊임없이 이어지는 것을 말합니다.
브라우저 같은 경우 자바스크립트로 구현할 수 있겠고 안드로이드도 그런 식으로 만드는 것이 가능하겠으나 많이 사용되고 있으므로 여러가지 툴이 존재합니다. 그 중 구글이 제공하는 Recycler View를 이용해서 만들어보겠습니다.

2. Recycler View 이용


우선 Recycler View 라고 구글에서 제공해주는 좋은 재료가 있습니다.


import android.support.v7.widget.RecyclerView;
cs


그 다음 Listener를 달아줍니다.


public abstract class EndlessScrollListener extends RecyclerView.OnScrollListener {
}
cs


이렇게 RecyclerView.onScrollListener를 extends하여 구현합니다.

여기에 작업을 해주면 됩니다. 세부적인 옵션을 설정하겠습니다.


    private int visibleThreshold = 2;
    private int currentPage = 1;
    private int previousTotalItemCount = 0;
    private boolean loading = true;
    private int startingPageIndex = 1;
    private RecyclerView.LayoutManager mLayoutManager;
cs


변수의 이름대로 옵션을 설정했습니다.

그 다음엔 Scroll에 필요한 Listener를 등록하기 위해 layoutManager를 작성합니다.


    protected EndlessScrollListener(LinearLayoutManager layoutManager) {
        this.mLayoutManager = layoutManager;
    }
    
    public EndlessScrollListener(GridLayoutManager layoutManager) {
        this.mLayoutManager = layoutManager;
        visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
    }
    
    public EndlessScrollListener(StaggeredGridLayoutManager layoutManager) {
        this.mLayoutManager = layoutManager;
        visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
    }
cs

이렇게 여러가지 매니저를 처리하도록 합니다. visibleThreshold와 layoutManager의 getSpanCount()를 이용해서 visibleThreshold에 다시 값을 지정합니다.


그 다음 스크롤 후 마지막 항목 차례에서 벌어질 이벤트를 처리합니다. 이 부분을 통해 무한으로 스크롤되는 유저 경험을 제공합니다. 마지막으로 보이게 되는 아이템과 그 아이템의 위치를 이용해서 for 문을 구성하고 maxSize 변수에 값을 넣어 이벤트를 만들 것입니다.


    @Contract(pure = true)
    private int getLastVisibleItem(int[] lastVisibleItemPositions) {
        int maxSize = 0;
        for (int i = 0; i < lastVisibleItemPositions.length; i++) {
            if (i == 0) {
                maxSize = lastVisibleItemPositions[i];
            } else if (lastVisibleItemPositions[i] > maxSize) {
                maxSize = lastVisibleItemPositions[i];
            }
        }
        return maxSize;
    }
cs


3. 마지막 항목에서의 이벤트

마지막 항목까지 스크롤 되었을 때의 이벤트를 조금 더 세부적으로 설정합니다. lastVisibleItemPosition 변수를 만들어서 처리합니다.


    @Override
    public void onScrolled(RecyclerView view, int dx, int dy) {
        int lastVisibleItemPosition = 0;
        int totalItemCount = mLayoutManager.getItemCount();
        
        if (mLayoutManager instanceof StaggeredGridLayoutManager) {
            int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
            // get maximum element within the list
            lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
        } else if (mLayoutManager instanceof GridLayoutManager) {
            lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
        } else if (mLayoutManager instanceof LinearLayoutManager) {
            lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
        }
        
        if (totalItemCount < previousTotalItemCount) {
            this.currentPage = this.startingPageIndex;
            this.previousTotalItemCount = totalItemCount;
            if (totalItemCount == 0) {
                this.loading = true;
            }
        }
        if (loading && (totalItemCount > previousTotalItemCount)) {
            loading = false;
            previousTotalItemCount = totalItemCount;
        }
        if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
            currentPage++;
            onLoadMore(currentPage, totalItemCount, view);
            loading = true;
        }
    }
cs


이렇게 한 후엔 스크롤이 반복적으로 reset될 state를 설정하면 됩니다.


    public void resetState() {
        this.currentPage = this.startingPageIndex;
        this.previousTotalItemCount = 0;
        this.loading = true;
    }
 
cs


그 다음에 마무리로 더 불러오기를 구현합니다. 여러가지 처리를 한 끝에 자동적으로 스크롤할 콘텐츠를 불러오게 됩니다. 사용자는 끊임없이 스크롤되는 것처럼 느끼게 됩니다.


    public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view);
cs


이렇게 끝맺을 수 있습니다.

지난 포스트에서 비율을 정하는 부분이 있었습니다.


        int width = opts.outWidth;
        int height = opts.outHeight;
 
        float sampleRatio = getSampleRatio(width, height);
cs


이 부분을 더 자세히 알아보겠습니다. getSampleRatio는 width와 height 값을 받는 함수입니다. java에서 기본으로 제공하는 함수가 아니므로 설명이 더 필요합니다.


1. getSampleRatio() 


    private float getSampleRatio(int width, int height) {
    }
cs


이 함수를 채워넣을 것입니다.


        final int targetWidth = 1280;
        final int targetheight = 1280;
        float ratio;
cs


우선 이렇게 인자를 형성합니다. 흔히 확인할 수 있는 화면 해상도인 1280 * 720 에서의 1280 입니다.


        if (width > height) {
            // 가로
            if (width > targetWidth) {
                ratio = (float) width / (float) targetWidth;
            } else ratio = 1f;
        } else {
            // 세로
            if (height > targetheight) {
                ratio = (float) height / (float) targetheight;
            } else ratio = 1f;
        }
cs


이렇게 가로와 세로의 경우의 수를 따져줍니다. 모바일 화면이니 1280이 그 기준이 됩니다.


    private File createFileFromBitmap(Bitmap bitmap) throws IOException {
        File newFile = new File(getActivity().getFilesDir(), makeImageFileName());
        FileOutputStream fileOutputStream = new FileOutputStream(newFile);
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
        fileOutputStream.close();
        return newFile;
    }
cs


이렇게 File에다가 Bitmap을 이용해서 함수를 만들어줍니다. getSampleRatio는 단순히 비율만을 결정하므로 파일을 가져와주는 과정이 필요합니다. FileOutPutStram(newFile)을 이용한 다음에 주어진 비트맵을 편집할 수 있도록 bitmap.compress를 사용해줍니다.


2. makeImageFileName()


    private String makeImageFileName() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd_hhmmss");
        Date date = new Date();
        String strDate = simpleDateFormat.format(date);
        return strDate + ".png";
    }
cs


여기서 FileName을 설정해줄 수 있습니다. format도 정해줄 수 있습니다. 이름을 정함과 동시에 포맷까지 결정하는 함수입니다. 이미지파일을 다룰 것이므로 png 확장자를 사용했습니다. SimpleDateFormat은 java에서 제공해줍니다.


import java.text.SimpleDateFormat;
import java.util.Date;
cs


이걸 이용하면 됩니다. 날짜 관련 자료들을 텍스트나 원하는 형식으로 편집할 때 유용하게 쓰입니다.

1. 준비물 

Bitmap을 이용할 것입니다. 그러므로 Bitmap관련 모듈들을 import하고 getBitmapFromUri를 새로 만듭니다.


1
2
3
4
5
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;  
      private Bitmap getBitmapFromUri(Uri uri) throws IOException {
    }
cs


그 다음에 FileDescriptor를 import 하고 새로 FileDescriptor를 써 줍니다.


1
2
3
4
5
6
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
        ParcelFileDescriptor parcelFileDescriptor =
                getActivity().getContentResolver().openFileDescriptor(uri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
cs


2. 비트맵 파일 편집


파일을 가져온 다음엔 BitmapFactory를 이용해서 세부 옵션을 설정합니다.


        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, opts);
cs


옵션은 따로 정해줄 수 있습니다.


        int width = opts.outWidth;
        int height = opts.outHeight;
 
        float sampleRatio = getSampleRatio(width, height);
 
        opts.inJustDecodeBounds = false;
        opts.inSampleSize = (int) sampleRatio;
 
cs


그 후에 Bitmap 스트림을 닫아줍니다.


        Bitmap resizedBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, opts);
        Log.d("Resizing", "Resized Width / Height : " + resizedBitmap.getWidth() + "/" + resizedBitmap.getHeight());
        parcelFileDescriptor.close();
        return resizedBitmap;
cs


3. 이미지 파일에 부여될 텍스트 편집

Bitmap으로 불러온 파일에 이름을 지정할 것이므로 아래의 코드를 activity에서 원하는 자리에 위치시킵니다. 부모레이아웃과 자식레이아웃을 설정해서 작업해봤습니다.


    public void onAddField(View v) {
        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        final View rowView = inflater.inflate(R.layout.field, null);
        parentLinearLayout.addView(rowView, parentLinearLayout.getChildCount());
        int count = parentLinearLayout.getChildCount();
        TextView result_field = findViewById(R.id.result);
        result_field.setText(Integer.toString((count-1)));
        View view = parentLinearLayout.getChildAt(count-1);
        if(view instanceof EditText){
            EditText last = (EditText)view;
            last.setSelection(0);
        }
    }
cs


그 다음 이름을 사용자로부터 받은 후 등록여부를 재차 확인하는 버튼을 구현합니다.


    @Override
    public void onBackPressed() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("You can Save your title");
        builder.setMessage("re-check your message?");
        builder.setPositiveButton("Enter"new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                finish();
            }
        });
        builder.setNegativeButton("Deny"new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });
        AlertDialog alert = builder.create();
        alert.show();
    }
}
cs


이렇게 긍정 버튼과 부정 버튼을 설정했습니다. 사용자가 Enter와 Deny 둘 중 하나를 선택하여 이름을 지정할 수 있습니다.

+ Recent posts