Dev./Android

여러 개의 ScrollView의 스크롤을 동기화(synchronization) 해보자

like miller 2012. 1. 31. 15:11

스크롤 뷰의 동기화? 여러 개의 스크롤 뷰가 한 화면에 있을 경우 1개의 스크롤 뷰만 스크롤 해도 다른 스크롤 뷰들이 같이 스크롤 되는 것을 말한다. 예를 들면 Beyond Compare, araxis merge 같은 파일 비교 merge툴에서 사용되는 기능 같은 것이다.


안드로이드 개발자 커뮤니티에서 질문을 보면 스크롤 뷰 여러 개의 스크롤을 동기화 할 수 없냐는 질문이 가끔 올라오곤 한다. 왜 굳이 여러 스크롤 뷰의  스크롤을 동기화 하려는지는 모르겠다. 큰 스크롤 뷰에 리니어레이아웃을 넣고 다시 리니어 레이아웃에 텍스트뷰를 넣으면 되는 것을......

하지만 가끔 그런 질문을 하는 사람을 위해 한 번 샘플을 만들어봤다.

1. 다른 뷰로 이벤트 전달하기
일단 여러 개의 스크롤 뷰의 스크롤을 동기화 하려면 사용자가 직접 터치하여 스크롤하는 이벤트를 받는 스크롤 뷰가 다른 여러 스크롤 뷰에 터치 이벤트를 전달해야 한다. 여기서 부터 시작을 해야 한다.


기본적으로 View에는 다른 뷰에 이벤트를 넘겨주는 메소드가 존재한다. 스크롤 뷰의 스크롤을 동기화 하기 위해서는 터치 이벤트를 다른 뷰로 넘겨야 한다.

(사용자가 터치한 후 드래그를 하는 이벤트 이니까 터치 이벤트를 넘김.)


이에 해당하는 메소드는 View.dispatchTouchEvent(MotionEvent event); 이다.
*http://developer.android.com/reference/android/view/View.html#dispatchTouchEvent%28android.view.MotionEvent%29

이제 메소드를 알았냈으니 한 번 동기화를 해보자.

2. 스크롤 뷰의 스크롤 동기화

화면 구성은 스크롤 뷰 2개를 왼쪽, 오른쪽에 넣고 2개의 스크롤 뷰의 스크롤을 동기화하는 옵션을 선택하는 라디오버튼을 상단에 넣어 구성하였다.


*화면 구성


*화면 구성 xml파일

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <RadioGroup
        android:id="@+id/sync"
           android:layout_width="fill_parent"
           android:layout_height="wrap_content"
           android:orientation="horizontal" >
           <TextView
               android:layout_width="fill_parent"
               android:layout_height="wrap_content"
               android:layout_weight="1"
               android:text="@string/sync"
               android:textSize="16sp"
               android:textColor="@color/white"
               android:paddingLeft="10dp"/>
        <RadioButton
            android:id="@+id/sync_true"
               android:layout_width="fill_parent"
               android:layout_height="wrap_content"
               android:layout_weight="1"
               android:text="@string/enable"
               android:checked="true"/>
        <RadioButton
            android:id="@+id/sync_false"
               android:layout_width="fill_parent"
               android:layout_height="wrap_content"
               android:layout_weight="1"
               android:text="@string/disable"/>
    </RadioGroup>
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="horizontal" >
        <ScrollView
            android:id="@+id/scroll1"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_weight="1">
            <TextView
                android:id="@+id/text1"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:textColor="@color/white" />
        </ScrollView>
       
        <ScrollView
            android:id="@+id/scroll2"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_weight="1">
            <TextView
                android:id="@+id/text2"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:textColor="@color/white" />
        </ScrollView>
    </LinearLayout>
</LinearLayout>


일단은 첫 번째 스크롤 뷰를 터치하여 움직이면 두 번째 스크롤 뷰도 같이 움직이게 하는 것 부터 해보자.


*SynchronizedScrollViewActivity.java


    private ScrollView mScroll1, mScroll2;                        //스크롤 뷰 1, 2
    private TextView mText1, mText2;                            //텍스트 뷰 1, 2


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        mScroll1 = (ScrollView)findViewById(R.id.scroll1);        //스크롤뷰 1번
        mScroll1.setOnTouchListener(mListener);                    //터치 이벤트 리스너 등록
       
        mScroll2 = (ScrollView)findViewById(R.id.scroll2);        //스크롤뷰 2번
        mScroll2.setOnTouchListener(mListener);                    //터치 이벤트 리스너 등록
       
        mText1 = (TextView)findViewById(R.id.text1);            //텍스트 뷰 1번
        mText2 = (TextView)findViewById(R.id.text2);            //텍스트 뷰 2번
       

        setTextFromAssetFile(mText1, "text1.txt");        //텍스트뷰에 텍스트 설정
        setTextFromAssetFile(mText2, "text2.txt");        //텍스트뷰에 텍스트 설정
    }


    //스크롤 뷰 터치 이벤트 리스너. 여기서 동기화를 해준다.
    private onTouchListener mListener = new onTouchListener() {
        @Override public boolean onTouch(View v, MotionEvent event) {
            int id =  v.getId();                    //이벤트 들어온 뷰의 아이디값

            //1번 스크롤 뷰가 터치 되면 2번 스크롤 뷰에 이벤트 전달
            if(id == R.id.scroll1)
                mScroll2.dispatchTouchEvent(event);    //터치 이벤트 전달

            return false;
        }
    };


첫 번째 스크롤 뷰로 두 번째 스크롤 뷰를 동기화 하는 방법을 알게 됐다. 그렇다면 두 번째 스크롤 뷰를 통해서도 첫 번째 스크롤 뷰를 동기화 해보자.

//1번 스크롤 뷰가 터치 되면 2번 스크롤 뷰에 이벤트 전달
if(id == R.id.scroll1)
    mScroll2.dispatchTouchEvent(event);    //터치 이벤트 전달

위 소스에서 else if로 두 번째 스크롤 뷰일 때 첫 번째 스크롤 뷰에게 이벤트를 전달하면 될 것이다.

//1번 스크롤 뷰가 터치 되면 2번 스크롤 뷰에 이벤트 전달
if(id == R.id.scroll1)
    mScroll2.dispatchTouchEvent(event);    //터치 이벤트 전달
else if(id == R.id.scroll2)                    //2번 스크롤 뷰이면
    mScroll1.dispatchTouchEvent(event);    //1번 스크롤 뷰에게 터치 이벤트 전달

이렇게 생각했다면 자신을 반성해야 합니다. 잘 생각해 보세요. 1번을 통해 2번에게 이벤트가 전달됩니다.
그렇다면 두 번째 스크롤 뷰 터치 이벤트 리스너에서 다시 첫 번째 스크롤 뷰에게 이벤트를 전달합니다.

즉, 1번 -> 2번 -> 1번 ->2번 이렇게 의도치 않게 무한 루프의 형태가 되게 될 것이다. 그리고 안드로이드는 StackOverflowError Exception을 토해 내겠죠.

그럼 어떻게 하면 2개의 스크롤 뷰 모두 다른 스크롤 뷰에게 이벤트를 전달하면서 무한 루프에 빠지지 않게 할 수 있을까? 터치이벤트 리스너에서 조건을 주면 해결이 된다. 사용자에 의해서 이벤트를 받는 뷰가 누구인지를 알면 됩니다.

사용자의 터치에 의해 이벤트 받는 뷰만 다른 뷰에게 이벤트를 전달하고 다른 뷰에 의해 이벤트를 받은 뷰는 이벤트를 전달하지 않게 한다.


*소스 수정

//전역변수(멤버필드) 추가

private int mTouchStartView = 0;                            //실제 사용자가 터한 뷰를 가르키는 변수


//터치 이벤트 리스터 수정

private onTouchListener mListener = new onTouchListener() {
    @Override public boolean onTouch(View v, MotionEvent event) {
        int id =  v.getId();                    //이벤트 들어온 뷰의 아이값
        int action = event.getAction();     //이벤트 동작(다운, 무브, 업 등.)

        //터치 다운이벤트가 들어오고, 기존에 터치된 뷰가 없으면
        //즉, 현재 이벤트가 들어온 뷰가 사용자가 직접 터치한 뷰이면
        if(action == MotionEvent.ACTION_DOWN && mTouchStartView == 0)
            mTouchStartView = id;    //뷰의 id값 저장.

        //사용자가 터치한 뷰가 스크롤뷰 1번이고 (2번에 이벤트를 전달하기위해 구분)
        //사용자가 직접 터치한 뷰이면 이벤트를 넘겨준다.
        //사용자가 직접 터치 하지 않고 다른 뷰가 이벤트를 넘겨줬을 경우는 패스
        if(mTouchStartView == R.id.scroll1 && mTouchStartView == id)
            mScroll2.dispatchTouchEvent(event);

        //2번 스크롤 뷰이면 1번에 이벤트 넘겨줌
        else if(mTouchStartView == R.id.scroll2 && mTouchStartView == id)
            mScroll1.dispatchTouchEvent(event);

        //터치가 끝나면 변수 값 초기화.
        //플링시 그 이벤트도 같이 전달하기 위해서 마지막에 검사.
        //플링은 무시하려면 위에 있는 터치 다운 이벤트 검사 바로 다음으로 옮기면 플링은 무시한다.
        if(action ==MotionEvent.ACTION_UP)
            mTouchStartView = 0;

        return false;
    }
};


전체 샘플 코드 첨부하였습니다.

*글과 자료는 출처만 밝히시면 얼마든지 가져다 쓰셔도 됩니다.

SynchronizedScrollView.zip



SynchronizedScrollView.zip
0.07MB