읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

사이드 프로젝트에서 커뮤니티 기능을 구현하자는 의견이 나왔다. 커뮤니티 기능은 사실 제대로 구현하려면 거의 준 프로젝트 급이라 따로 페이지를 만들어서 구현하는게 낫겠다는 판단이 섰다. 시연용 앱만 제작하라고 주문받았지만 원래 관심이 있었던 분야라 기능적 구현 완성을 목표로 정리를 시작하려 한다.

이번 포스팅은 RecyclerView가 포함된 Custom된 Dialog를 AlertDialog 방식으로 띄우는 내용입니다.

이전 포스팅 - Android | Activity Sliding Open하기와 이어집니다.

동작 설명

특정 Layout을 클릭하면 Dialog를 출력하여 Topic을 선택할 수 있게 기능을 제공한다.

UI 배치하기

먼저 어떤 상황에서 Dialog를 불러올 지 결정해야 한다. 필자의 프로젝트에서는 포스팅 토픽을 정하기 위해 구현하기로 했으므로 PostingActivity를 생성한 뒤 특정 Layout에서 클릭 이벤트가 발생하면 AlertDialog 형태로 띄우기로 했다.

layout_posting.xml

                <LinearLayout
                    android:id="@+id/linearLayoutTopicPosting"
                    android:layout_width="0dp"
                    android:layout_height="0dp"
                    android:layout_marginEnd="5dp"
                    android:clickable="true"
                    android:focusable="true"
                    android:orientation="horizontal"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintEnd_toStartOf="@+id/posting_title_vg_30"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent">

                    <TextView
                        android:id="@+id/textViewPostingTopic"
                        android:layout_width="wrap_content"
                        android:layout_height="match_parent"
                        android:ellipsize="end"
                        android:gravity="center"
                        android:maxLines="1"
                        android:paddingLeft="8dp"
                        android:text="카테고리 없음"
                        android:textColor="#CCCCCC" />

                    <ImageView
                        android:id="@+id/imageView3"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center|right"
                        android:layout_weight="1"
                        android:src="@drawable/down_arrow" />
                </LinearLayout>

activity_posting.xml의 일부분을 발췌했다. 포스트 토픽을 나타내는 Linear Layout을 정의한 코드이다. 해당 LinearLayout에 clickable, focusable 속성을 부여하여 클릭 이벤트를 정의할 예정이다.

alert_dialog_border.xml

CardView는 그 자체로 View를 만들어 RecyclerView에 넣는 방식이 일반적이다. 그렇다면 테두리가 직각이 아닌 Dialog는 다른 방법으로 구현하는 게 오류가 비교적 덜 발생하겠다는 생각이 들었다. 아예 dialog 자체를 출력할 때 테두리를 둥그렇게 만들면 되기에 drawable 폴더에 xml파일을 생성하여 아래와 같이 정의했다.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@android:color/white" />
    <corners android:radius="20dp" />
</shape>

radius 20dp는 cardview radious와 동일하여 cardview가 띄워진 느낌을 줄 수 있다.

dialog_layout.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"
    android:background="@drawable/alert_dialog_border"
    android:layout_width="wrap_content"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/dialogRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Dialog 안에 어떤 내용을 넣을 지 결정해야 한다. 토픽 목록을 사용자가 선택하게끔 제공해야 하므로 RecyclerView를 정의했다.

recyclerview_topic_item.xml

recyclerView에 데이터를 넣고 보여줄 때 각 item에 대한 View를 정의해야 한다. 단순히 텍스트만 나열해서 클릭 이벤트를 부여할 예정이므로 textView 한개만 정의했다.

<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp">

    <TextView
        android:id="@+id/textViewRecyclerItem"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:layout_marginBottom="6dp"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

backgournd를 담당하는 ConstrainLayout의 height는 wrap_content로 해야 한다. 그렇지 않으면 크기가 RecyclerView의 높이에 맞춰서 넓어지기 때문이다.

코드 작업하기

DialogRecyclerAdapter

RecylcerView에 제공하고자 하는 Topic을 넣고 Custom Dialog를 출력해야 한다. 먼저 RecyclerView에 데이터를 넣어보자.

public class DialogRecyclerAdapter extends RecyclerView.Adapter<DialogRecyclerAdapter.ViewHolder> {
    private ArrayList<String> mData = null;

    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView textView;
        ViewHolder(View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.textViewRecyclerItem); // 각 item View에 대해
            textView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    String topic = mData.get(getAdapterPosition());
                    PostingActivity.textViewPostingTopic.setText(topic);
                    if (topic.equals("카테고리 없음")) { // 카테고리 없음 선택 시
                        PostingActivity.textViewPostingTopic.setTextColor(Color.LTGRAY);
                        // 회색 처리
                    } else {
                        PostingActivity.textViewPostingTopic.setTextColor(Color.BLACK);
                        // 그 이외엔 검은색
                    }
                    PostingActivity.dialog.dismiss(); // dialog 종료
                }
            });
        }
    }
    public DialogRecyclerAdapter(ArrayList<String> list) {
        mData = list; // 입력받은 list를 저장
    }

    @Override
    public DialogRecyclerAdapter.ViewHolder onCreateViewHolder (ViewGroup parent, int viewType) {
        Context context = parent.getContext(); // parent로부터 content 받음
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.recyclerview_topic_item, parent, false);
        // 각 item의 View는 이전에 정의했던 item layout을 불러옴
        DialogRecyclerAdapter.ViewHolder vh = new DialogRecyclerAdapter.ViewHolder(view);
        return vh; // ViewHolder 반환
    }

    @Override
    public void onBindViewHolder(DialogRecyclerAdapter.ViewHolder holder, int position) {
        String text = mData.get(position); // 어떤 포지션의 텍스트인지 조회
        holder.textView.setText(text); // 해당 포지션의 View item에 텍스트 입힘
    }

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

위의 코드는 일반적인 RecyclerViewAdapter 코드 형태이다. ViewHolder 클래스에 각 Item에 대한 클릭 이벤트 리스너를 정의해서 어떻게 동작할 것인지 작성하고 onBindViewHolder에서 각 position 별 View에 대해 어떻게 content를 입힐 것인지 작성한다. onCreateViewHolder는 각 item들이 어떤 View를 적용해서 출력될 것인지 명시한다.

PostingActivity

public class PostingActivity extends AppCompatActivity {
    private LinearLayout linearLayoutTopicPosting; // 클릭 이벤트 받는 곳
    private RecyclerView dialogRecyclerView; // RecyclerView
    public static TextView textViewPostingTopic; // 선택된 Topic 보여주는 곳
    public static Dialog dialog; // 출력할 Dialog 객체
    String[] text = {"A Topic", "B Topic", "C Topic", "D Topic", "E Topic", "카테고리 없음"}; // 제공할 Topic

Custom Dialog에 필요한 코드만 가져왔다. 이제 코드를 하나씩 뜯어보면서 View와 이벤트 리스너를 정의하자.

        linearLayoutTopicPosting = findViewById(R.id.linearLayoutTopicPosting);
        linearLayoutTopicPosting.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showAlertDialogTopic();
            }
        });

"포스팅 토픽" 만을 보여주는 TextView에서 클릭 리스너를 받으면 범위가 너무 좁다 따라서 해당 TextView가 들어있는 Layout 전체에서 클릭을 받는 게 UX 상 좀 더 유리하다.

    private void showAlertDialogTopic() {
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        dialog = new Dialog(this);

        display.getRealSize(size);
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams();

        LayoutInflater inf = getLayoutInflater();
        View dialogView = inf.inflate(R.layout.dialog_layout, null);
        // Dialog layout 선언

        lp.copyFrom(dialog.getWindow().getAttributes());
        int width = size.x;
        lp.width = width * 80 / 100; // 사용자 화면의 80%
        lp.height = WindowManager.LayoutParams.WRAP_CONTENT; // 높이는 내용 전체 높이만큼
        dialog.setContentView(dialogView); // Dialog에 선언했던 layout 적용
        dialog.setCanceledOnTouchOutside(true); // 외부 touch 시 Dialog 종료
        dialog.getWindow().setAttributes(lp); // 지정한 너비, 높이 값 Dialog에 적용
        ArrayList<String> arrayList = new ArrayList<>(); // recyclerView에 들어갈 Array
        arrayList.addAll(Arrays.asList(text)); // Array에 사전에 정의한 Topic 넣기
        /*
        다음 4줄의 코드는 RecyclerView를 정의하기 위한 View, Adapter선언 코드이다.
        1. RecyclerView id 등록
        2. 수직방향으로 보여줄 예정이므로 LinearLayoutManager 등록
           2차원이면 GridLayoutManager 등 다른 Layout을 선택
        3. adapter에 topic Array 넘겨서 출력되게끔 전달
        4. adapter 적용
        */
        dialogRecyclerView = (RecyclerView) dialogView.findViewById(R.id.dialogRecyclerView);
        dialogRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        DialogRecyclerAdapter adapter = new DialogRecyclerAdapter(arrayList);
        dialogRecyclerView.setAdapter(adapter);
        dialog.show(); // Dialog 출력
    }

코드가 좀 길어졌다. 먼저 Display를 선언한 이유는 Dialog 안에 들어있는 값에 따라서 너비가 천차만별이라 조금 일관된 기준이 필요하여 사용자 화면 크기를 받아 일정 비율의 너비값을 갖게끔 하기 위함이다. 그리고 Point는 앞의 사용자 화면 값을 받기 위한 객체로 이해하면 된다. 코드에 주석을 첨부해봤으나 혹 이해가 안된다면 댓글로 달아주세요.

동작 결과

Android_RecyclerView_Custom_Dialog_01

+ Recent posts