갤러리형 리스트 – Card Flip Animation List

갤러리형 리스트 – Card Flip Animation List 만들기

3D로 카드가 넘어가는 듯한 효과를 내는 CardFlip 애니메이션 리스트 입니다.
요즘 안드로이드 앱에서 많이 사용하는 Flip 형태의 애니메이션 혹은 
유사한 형태의 애니메이션을 구현해야 한다면
여기서 사용된 소스를 응용하면 됩니다.플립 애니메이션이 겹치는 부분이 딱 맞아 떨어지지는 않지만
scaling 을 사용해서 손 좀 보시면 매끄럽게 될 겁니다.


소스 다운로드 :

 

주요 구현 내용 :

 – Strechable list (Custom list) 를 구현 예제에서 사용된 기본 뼈대가 그대로 사용됩니다.

   (AdapterView 를 상속해서 Custom list를 만듬. 일반 리스트처럼 어댑터 붙여서 사용.)

   고로, 중복되는 부분에 대한 설명은 생략.

 – 각 아이템 그래픽에 효과를 주기 위해 drawChild()를 상속하고 

   child view의 비트맵을 얻어와서 변형합니다.

 – 3D 효과, scaling, 투명도 조절을 위해 Camera, Matrix, Paint 클래스를 사용합니다.

   (본 예제에서는 scaling, 투명도 조절 부분은 빠져 있습니다. 직접 수정해서 써보세요.)

 – 드래그 속도를 컨트롤 할 때 이용하기 위해 VelocityTracker 클래스를 사용합니다.


 

:: 소스 분석

 

 

1. 테스트 앱 준비

 

Layout

 – activity_main.xml : 리스트로 화면을 채우는 레이아웃.

 – card.xml : 리스트에 꽉 차도록 가로/세로 match_parent 로 설정

Activity 코드

 – MainActivity.java : 테스트용 어댑터와 데이터 클래스 포함

 

 

2. CardFlipper (AdapterView 상속) 기본 뼈대

 

현재 선택된 메인 카드와  드래그 동작할 때 보여지는 카드를 처리하기 위해

항상 3개의 View를 리스트에 child view로 설정합니다.

그리고 현재 화면상에 보여지는 current view가 adapter의 어떤 아이템과 같은지 표시하기 위해

mCurrentIndex 변수로 저장해 둡니다.

 

 

    private View mPrevChild = null;

    private View mCurChild = null;

    private View mNextChild = null;

 

    private int mCurrentIndex = 0;

 

 

드래그로 화면이 이동 할 경우 화면이 움직일 거리를 나타내는

 

mDragPosition 변수를 둡니다.

터치나 auto scrolling 에 의해서 화면이 움직일 때 이 변수로 제어합니다.

 

 

 private int mDragPosition = 0;

 

 

드래그 시작, 종료, auto scrolling 등의 상태를 참조하기 위해 아래 파라미터를 정의합니다.

 

 

 

    /* Flip 상태 */

    private static final int FLIP_STATUS_IDLE = 0; // IDLE : 사용자 드래그, 화면 업데이트 없음

    private static final int FLIP_STATUS_MOVE = 1;        // 드래그 시작 상태

    private static final int FLIP_STATUS_AUTO_MOVE = 11;// 드래그 종료 후 자동 스크롤

  

    private int mFlipStatus = FLIP_STATUS_IDLE;

 

 

3. Add and remove view, layout items

 

화면 업데이트는 아래 순서대로 진행합니다.

 

1. Adapter가 설정되거나 Drag 이벤트가 들어올 경우 

 – 화면이 이동할 거리 계산 후 requestLayout() 호출

 – 이후 화면 업데이트를 위한 메인 메서드인 onLayout() 이 호출됨.

 

2. onLayout()

2-1. redrawLayout()

 – 아이템이 없어 초기화가 필요한 경우  아이템 추가

 – 드래그로 화면 전환이 이루어 질 경우 아이템을 추가/삭제 : addAndRemoveItems()

2-2. layoutItems()

 – previous, current, next 아이템의 위치를 조정 (리스트 view에 상대적인 위치)

 – 화면에 보이지 않는 아이템을 화면 밖으로 이동

 

3. drawChild()

 – 아이템에 3D 효과를 주기 위해 child view의 Bitmap 을 얻어옴.

 – android.graphics.Camera 인스턴스를 생성해서 

   아이템들을 Y축 기준으로 회전하는 것이 핵심

 – 필요에 따라 Matrix, Paint 를 이용해서 scaling, Alpha 조절

 

 

     /****************************************************

     * 자식 뷰를 그리는 루틴. 자식 뷰에 특별한 효과를 넣고 싶다면 이 메소드를 상속해서 수정

     * Drawing cache에서 자식 뷰의 bitmap을 얻어옴

     * child.setDrawingCacheEnabled(true) 설정이 되어 있어야 bitmap 추출 가능

     ***************************************************/

    @Override

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

        

        final Bitmap bitmap = child.getDrawingCache();

        

        // 비트맵이 추출되지 않았거나 화면에 보이지 않는 아이템, 드래그 움직임이 없는 경우는

        // 아무런 애니메이션 효과를 주지 않음

        if (bitmap == null 

                || child.getLeft() <= getWidth()*-1

                || child.getLeft() >= getWidth()

                || mDragPosition == 0) {

            // super.drawChild() 호출 – 애니메이션 효과 없음.

            return super.drawChild(canvas, child, drawingTime);

        }

 

        // 계산에 필요한 파라미터 생성

        final int top = child.getTop();

        final int left = child.getLeft();

        int centerX = child.getMeasuredWidth()/2;

        int centerY = child.getMeasuredHeight()/2;

 

        // 현재 드로잉 대상이 prev/cur/next 중 어떤 아이템인지 판별

        float cardPosition = 1f;

        if(mDragPosition < 0) {

            if(left > 0) 

                cardPosition = -1f;

        }

        else if(mDragPosition > 0) {

            if(left < 0) 

                cardPosition = -1f;

        }

        

        // 카메라 효과를 (세로 축 회전) 위해 회전각 계산

        float rotation;

        if(cardPosition == -1f) {

            if(mDragPosition > 0) {

                rotation = (65f – 65f/getWidth() * mDragPosition) * -1; //이전 item 회전각 (-65'~0')

            }

            else {

                rotation = (65f – 65f/getWidth() * mDragPosition * -1);//다음 item 회전각 (65'~0')

            }

        }

        else {

            rotation = ( 65f/getWidth() * mDragPosition );// 현재 item 회전각 (-65' ~ 65')

        }

 

        // 카메라 효과 (이미지를 Y축 중심 회전)

        if (mCamera == null) {

            mCamera = new Camera();

        }

        mCamera.save();

 

        //mCamera.translate(0, 0, centerX);

        mCamera.rotateY(rotation);

        //mCamera.translate(0, 0, -centerX);

        

        // Matrix 인스턴스 생성. 이미 생성되어 있다면 reset

        if (mMatrix == null) {

            mMatrix = new Matrix();

        } else {

            mMatrix.reset();

        }

 

        mCamera.getMatrix(mMatrix);        // 카메라 설정을 matrix에 저장

        mCamera.restore();

        

        // 이미지 scaling이 필요할 때 사용

        // float scale = 1; //1 / ( 1 + Math.abs(mVelocity) / VELOCITY_MIN_THRESHOLD * SCALE_FACTOR  );

        

        // Matrix를 이용한 이미지 변환 커맨드 설정

        mMatrix.preTranslate(-centerX, -centerY);  //이미지 좌 상단으로 원점 설정 

        // mMatrix.postScale(scale, scale);        // scaling

        mMatrix.postTranslate(left + centerX, top + centerY);  // 아이템의 원래 위치로 이동 

        

        // Paint 설정 : Alpha-투명도 적용이 필요할 경우

        if (mPaint == null) {

            mPaint = new Paint();

            mPaint.setAntiAlias(true);

            mPaint.setFilterBitmap(true);

            mPaint.setAlpha(0xFF);

        }

        

        // Matrix 설정에 따라 이미지를 캔버스에 그림.

        canvas.drawBitmap(bitmapmMatrixmPaint);

        

        return false;

    }

 

 

4. Touch event, Runnable

 

속도에 따라 flip 동작을 주기 위해 VelocityTracker를 사용합니다.

드래그가 완료되면 조건에 따라 다음 아이템으로 이동하던지 원래 위치로 돌아옵니다.

 

 

     /**

     * 터치 이벤트 종료 루틴.

     * 아이템을 원위치 시키거나 다음 아이템 전환 되도록 runnable 실행.

     */

    private void endTouch(MotionEvent event) {

        // 속도 계산 – 일정 속도 이상인 경우 flip 동작

        mVelocityTracker.computeCurrentVelocity(UNIT_PIXELS_PER_MILLI);

        mVelocity = mVelocityTracker.getXVelocity();

        

        mVelocityTracker.recycle();

        mVelocityTracker = null;

 

        // 오른쪽으로 드래그가 일정 수준 이상인 경우 이전 아이템으로 자동 스크롤

        if(mDragPosition > getWidth()/2) {

            mVelocity = VELOCITY_THRESHOLD;

            runAutoScrolling(getWidth());

        }

        // 왼쪽으로 드래그가 일정 수준 이상인 경우 다음 아이템으로 자동 스크롤

        else if(mDragPosition < getWidth()/2*-1) {

            mVelocity = VELOCITY_THRESHOLD*-1;

            runAutoScrolling(getWidth()*-1);

        }

        // 오른쪽 드래그 속도가 일정 수준 이상인 경우 이전 아이템으로 자동 스크

        else if(mVelocity > 0 && mVelocity >= VELOCITY_THRESHOLD) {

            runAutoScrolling(getWidth());

        }

        // 왼쪽 드래그 속도가 일정 수준 이상인 경우 다음 아이템으로 자동 스크롤

        else if(mVelocity < 0 && mVelocity <= VELOCITY_THRESHOLD*-1) {

            runAutoScrolling(getWidth()*-1);

        }

        // 현재 아이템의 원점으로 자동 스크롤

        else {

            if(mDragPosition < 0)

                mVelocity = VELOCITY_THRESHOLD;

            else

                mVelocity = VELOCITY_THRESHOLD * -1;

            runAutoScrolling(0);

        }

    }

 

 

 

드래그 종료되면 자동으로 원 위치로 이동하거나 다음 아이템으로 Flip 하기 위해서

Runnable 객체를 생성해서 반복 실행시킵니다.

 

 

     /**

     * Auto scrolling 이 시작될 수 있도록 설정.

     * Auto scrolling 처리하는 메인 루틴

     */

    private void runAutoScrolling(int targetPosition) 

    {

        mAutoScrollDestination = targetPosition;

        mPrevTime = AnimationUtils.currentAnimationTimeMillis();

        

        // 자동으로 스크롤이 되도록 Runnable 생성

        if (mScrollingRunnable == null) {

            mScrollingRunnable = new Runnable() {

 

 

                public void run() {

                    long time = AnimationUtils.currentAnimationTimeMillis();

                    float distance = mVelocity * (time – mPrevTime) * DISTANCE_CONVERT_FACTOR;

                    

                    if(mAutoScrollDestination == 0) {

                        // 원점으로 스크롤이 완료되는 시점인지 체크해서 

                        // 마지막 화면 업데이트 이후 runnable 이 실행되지 않도록 함.

                        if(mDragPosition*(mDragPosition+distance) < 0 || mDragPosition+distance == 0) {

                            scrollList(mDragPosition*-1);

                            mFlipStatus = FLIP_STATUS_IDLE;

                            return;

                        }

                        // 이미 현재 아이템의 원점으로 돌아온 경우

                        else if(mDragPosition == 0) {

                            mFlipStatus = FLIP_STATUS_IDLE;

                            return;

                        }

                    }

                    scrollList(distance);        // 리스트 화면 업데이트 요청

    

                    // 화면 업데이트 이후 계속해서 runnable 이 실행될 수 있도록 예약

                    // 이전, 다음 화면으로 이동이 완료된 경우는 실행하지 않음.

                    if(mFlipStatus == FLIP_STATUS_AUTO_MOVE

                            && mDragPosition < getWidth() && mDragPosition > getWidth()*-1) {

                        postDelayed(this, REDRAW_DELAY_IN_MILLI);

                    }

                }

            };

 

 

        }

        

        // Auto scrolling 상태로 변경 후 runnable 실행.

        mFlipStatus = FLIP_STATUS_AUTO_MOVE;

        post(mScrollingRunnable);

    }

 

 

5. ETC

 

간단히 얘기하면 Camera 인스턴스로 Y축 회전량만 잘 계산하면 됩니다만…

회전 될 경우 bitmap 크기가 변하기 때문에 두 개 view 의 간격을 잘 조절해야 매끄럽게 보입니다.

이 부분은 직접 수정하셔야 겠습니다.

 

여하튼 Flip 형태의 애니메이션 구현에 필요한 대강의 내용은 소스에 다 들어있으니

원하는 형태로 응용해서 쓰시면 멋진 커스텀 뷰를 만드실 수 있습니다!

 

 

 

Post Author: TORTUGA

TORTUGA
궁금하신 점은 새로 개편한 홈페이지의 QnA 게시판을 이용해주세요!!!!!!! http://www.hardcopyworld.com/gnuboard5/bbs/board.php?bo_table=qna

댓글 남기기

이메일은 공개되지 않습니다.