마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

레이아웃의 높이를 구하고싶습니다.

0 추천

다이얼로그 프래그먼트를 사용하고 이 다이얼로그는 리사이클러뷰로 이루어집니다,

아이템은 딱 5개만 쓸거에요.

그런데 다이얼로그의 사이즈를 이 리사이클러뷰를 구성하는 아이템의 높이로 설정하고싶어요

그래서 높이를 구해야하는데.. 이게 잘못된건지 아무 화면도 뜨지않네요..어디가 잘못됐나요?ㅠ

다이얼로그프래그먼트.java

public class WritingCommentDialogFragment extends DialogFragment implements CommentModel.EditInputListener {
    OnBackPressedCallback callback;

    LinearLayout mContainer; // input layout
    CommentModel commentItem;
    List<String> commentlist;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_writing_comment_dialog, container, false);

        bindViews(view);
        addCommentItem(); // 첫아이템
        return view;
    }

    private void bindViews(View v) {
        mContainer = v.findViewById(R.id.container);
        commentlist = new ArrayList<>();
    }

    private void addCommentItem() {
        commentItem = new CommentModel(getLayoutInflater(), null);
        commentItem.addListener(this);
        commentItem.setFocus();
        mContainer.addView(commentItem.getRootView());
    }

    @Override
    public void OnEnterPressed() {
        if (mContainer.getChildCount() >= 5) {
            return;
        }
        addCommentItem();
    }

    @Override
    public void OnDeleteKeyPressed() {
        if (mContainer.getChildCount() <= 1) {
            return;
        }

        if(mContainer.hasFocus()) {
            mContainer.removeView(mContainer.getFocusedChild());
        }
    }
    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        Dialog dialog = super.onCreateDialog(savedInstanceState);
        dialog.setCanceledOnTouchOutside(false);
        return dialog;
    }
}

다이얼로그프래그먼트.xml

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context=".fragment.WritingCommentDialogFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/comment_rv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

 

아이템.xml

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context=".fragment.WritingCommentDialogFragment">

    <EditText
        android:id="@+id/comment_edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:gravity="center_vertical"
        android:drawableLeft="@drawable/ic_bullet_point"
        android:drawablePadding="5dp"
        android:layout_marginHorizontal="10dp"
        android:background="@null"
        android:textSize="15sp"
        android:inputType="text"
        android:maxLines="1"
        android:maxLength="22"
        android:imeOptions="actionNone"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

 

codeslave (3,940 포인트) 님이 2021년 3월 4일 질문
codeslave님이 2021년 3월 7일 수정
아이템을 5개만 사용하신다면 리사이클러뷰보다는 EditText나 아이템.xml같은 형태의 커스텀 뷰를 사용하시는 편이 더 낫습니다. 아이템 숫자가 너무 적어 뷰를 재사용하지 못하므로 리사이클러뷰를 사용해서 얻는 이점이 없습니다. 리사이클러뷰를 안쓰는 쪽이 코드량도 더 적고 컨트롤도 편할 겁니다. 그리고 아이템.xml에서 바깥 레이아웃이 필요하시면 그냥  FrameLayout을 사용하시면 됩니다. ContraintLayout은 여기에서는 처리비용이 높은 옵션입니다.
리사이클러뷰를 상요하지 않으시면 질문하신 내용도 바로 해결이 될 겁니다.
감사합니다. 지금 원래는 RecyclerView까지 구현을 해놓은 상태입니다.
다이얼로그창을 열면 위 아이템이 하나 있는 상태이고 이 아이템은 최대 한줄만 입력가능합니다.  EnterKey를 누르면 다음 아이템이 생성되어 또 입력할 수있는 아이템이 생성됩니다. (코멘트 입력하는 아이템입니다).
이런 아이템이 5개까지 생성가능하구요.
그런데 선생님께서 재활용이 안되니 비효율적이라 하셔서, 리사이클러뷰 말고
다른 방면으로 EnterKey 눌렀을때 아이템이 추가되는 방법을 찾아야하는데,
addView()가 생각납니다.
괜찮은 방법일까요?

2개의 답변

0 추천

네. 맞습니다. 커스텀 컴포넌트를 통해서도 원하는 기능을 구현하실 수 있습니다. 저는 다른 접근방법을 보여드리도록 할게요.

 

public class CommentItem {

    interface Listener {
        void onEnterPressed();
    }

    @Nullable
    private CommentItem.Listener listener;
    private EditText commentEdit;

    private final View rootView;

    public CommentItem(@NonNull LayoutInflater layoutInflater, @Nullable ViewGroup parent) {
        rootView = layoutInflater.inflate(R.layout.item_comment, parent, false);

        initView();
    }

    private void initView() {
        commentEdit = this.findViewById(R.id.comment_edit);
        commentEdit.setSingleLine(true);
        commentEdit.setOnEditorActionListener((v, actionId, event) -> {
            if (actionId == EditorInfo.IME_ACTION_NEXT) {
                if (listener != null) {
                    listener.onEnterPressed();
                    return true;
                }
            }
            return false;
        });
    }

    public void addListener(CommentItem.Listener listener) {
        this.listener = listener;
    }

    public void removeListener() {
        this.listener = null;
    }

    public void setFocus() {
        commentEdit.requestFocus();
    }

    public View getRootView() {
        return rootView;
    }

    private Context getContext() {
        return rootView.getContext();
    }

    private <T extends View> T findViewById(@IdRes int id) {
        return rootView.findViewById(id);
    }
}



public class MainActivity extends AppCompatActivity implements CommentItem.Listener {

    private LinearLayout commentContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        bindViews();
        addCommentItem();
    }

    private void bindViews() {
        commentContainer = findViewById(R.id.comment_container);
    }

    private void addCommentItem() {
        CommentItem commentItem = new CommentItem(getLayoutInflater(), null);
        commentItem.addListener(this);
        commentItem.setFocus();
        commentContainer.addView(commentItem.getRootView());
    }

    @Override
    public void onEnterPressed() {
        if (commentContainer.getChildCount() >= 5) {
            return;
        }

        addCommentItem();
    }
}

 

spark (227,470 포인트) 님이 2021년 3월 5일 답변
선생님 감사합니다..저는 단순 addView사용만 생각했고 하는 과정중에 개별 리스너를 생각못해 첫번째 아이템에서만 이벤트가 발생해 어려움을 겪고 있엇는데..
이런방법이 있었군요..작성한 멘트를 저장할 String 변수를 아이템필드로 선언하거나 등 약간만 수정해서 사용하였습니다 .
매번 많이 배우고 느끼고갑니다 감사합니다.
선생님 죄송한데 코드를 다시보다가 잠깐 헷갈리는 부분이 생겨 여쭤봅니다.
선생님 코드에서 commentContainer.addView(commentItem.getRootView());
부분에서 어째서 edittext마다 키 리스너가 발생하는지 궁금합니다.
이 코드는 생성되는 edittext(아이템)마다 키 이벤트를 가지고 있습니다.
이걸 위해서 CommentItem이라는 클래스를 정의하시고 그 안에 키 이벤트 인터페이스를 정의하셨죠.
그런데 addView()라는게 제가 알기로는 부모뷰에 View를 추가하는 것입니다.
키이벤트가 발생할때마다 새로운 CommentItem을 생성해서 Comment안에서
인플레이션한 레이아웃 뷰를 getRootView()를 통해서 가져오잖아요?
그런데 이때 getRootView() 해서 가져오는 뷰는.. 뷰 자체이지 Commentitem이 아니지 않나요?
그냥 레이아웃파일을 인플레이션해서 뷰를 리턴받은것인데 어째서 이 뷰가
정의한 키 이벤트까지 가지고 있는지(동작하는지) 궁금합니다..
그리고 추가 질문으로..삭제이벤트 처리를 해야해서..
추가된 뷰에서 CommentItem을 불러와야하는데 이게 각 뷰마다 CommentItem을 불러오는것을 고전하다가 방법을 찾지못해 getFocusedChild()를 이용해서
뷰를 삭제하는 방식을 찾았습니다..

추가된 뷰에서 CommentItem을 찾지못하는 이유가.. 제 생각에는 위 질문과 관련있는 질문일수도 있는데.. addView한 것이 CommentItem이 아니라 뷰 자체이기때문에 현재 포커스된 CommentItem은 찾을수가 없는게 아닌가 생각됩니다..
개인적으로 가져올 수 있는 방법이 있다면 가져오고싶은데..
이유는 다이얼로그를 종료하면 해당 코멘트들을 List에 저장해 액티비티로 넘겨주려고 생각중이거든요..추측이 맞나요..ㅠㅠ

그래서 추가된 뷰에서 저장할때 사용된 각 뷰의 CommentItem 참조를 알수 있는방법이 있을까요..? 이것만 알면 어떻게 비교해서 뷰의 저장삭제에 용이할것같은데 말이죠 ㅠㅠ
질문의 포커스가 뷰 높이를 구하는 부분에 있었기 때문에. 그 부분에 맞춰서 답글을 달았던 건데요. 뷰와 로직을 분리되어야 하기 때문에, 뷰를 가지고 코멘트 데이터를 접근하려고 하시면 문제가 많이 발생합니다. Comment라는 코멘트 정보를 가지고 있는 데이터 클래스를 만드시고 이걸 List<Comment>  같은 콜렉션 같은 구조로 MainActivity에서 관리하시는 로직이 필요합니다. 물론 이걸 분리해서 따로 관리할 수 있는 클래스를 만드신 다음 더 좋겠죠. 제가 테스트를 해보고 간단한 예제를 올려 드릴게요.
0 추천

이렇게 하시면 됩니다. CommentItem 이 하는 일을 들여다 보는 액티비티가 하는 일과 아주 비슷합니다.  레이아웃을 inflate하고 이걸 뷰트리에 붙여넣고, 필요한 유저 interaction 을 처리하는 거죠. 기존 CommentItem은 건드리지 마시고 다음과 같이 생성된 CommentItem을 관리하는 코드를 넣으시면 됩니다.

 

item_comment.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    android:orientation="horizontal"
    android:padding="8dp"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/comment_edit"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:imeOptions="actionNext"
        android:inputType="text"
        android:maxLength="22"
        android:singleLine="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Hello World!" />

    <ImageButton
        android:id="@+id/delete_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/ic_menu_delete" />

</LinearLayout>


public class Comment {
    private int id;
    private String content;

    public Comment(int id, String content) {
        this.id = id;
        this.content = content;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Comment comment = (Comment) o;
        return id == comment.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}



public class CommentItem {

    interface Listener {
        void onCommentChanged(CommentItem item);
        void onEnterPressed(CommentItem item);
        void onDeleteClicked(CommentItem item);
    }

    @Nullable
    private CommentItem.Listener listener;
    private EditText commentEdit;
    private ImageButton deleteBtn;

    private final View rootView;
    private int commentId;

    public CommentItem(@NonNull LayoutInflater layoutInflater, @Nullable ViewGroup parent) {
        rootView = layoutInflater.inflate(R.layout.item_comment, parent, false);
        initView();
    }

    private void initView() {
        commentEdit = this.findViewById(R.id.comment_edit);

        commentEdit.setSingleLine(true);
        commentEdit.setOnEditorActionListener((v, actionId, event) -> {
            if (actionId == EditorInfo.IME_ACTION_NEXT) {
                if (listener != null) {
                    listener.onEnterPressed(this);
                    return true;
                }
            }
            return false;
        });

        commentEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            @Override
            public void afterTextChanged(Editable s) {
                if (listener != null) {
                    listener.onCommentChanged(CommentItem.this);
                }
            }
        });

        deleteBtn = this.findViewById(R.id.delete_button);
        deleteBtn.setOnClickListener(v -> {
            if (listener != null) {
                listener.onDeleteClicked(this);
            }
        });
    }

    public void addListener(CommentItem.Listener listener) {
        this.listener = listener;
    }

    public void removeListener() {
        this.listener = null;
    }

    public void setFocus() {
        commentEdit.requestFocus();
    }

    public int getCommentId() {
        return commentId;
    }

    public void setCommentId(int id) {
        this.commentId = id;
    }

    public String getText() {
        return commentEdit.getText().toString();
    }

    public void setText(String text) {
        commentEdit.setText(text);
    }

    public Comment toComment() {
        return new Comment(commentId, getText());
    }

    public View getRootView() {
        return rootView;
    }

    private Context getContext() {
        return rootView.getContext();
    }

    private <T extends View> T findViewById(@IdRes int id) {
        return rootView.findViewById(id);
    }

    public static CommentItem newCommentItem(LayoutInflater layoutInflater, ViewGroup parent, int id, CommentItem.Listener listener) {
        CommentItem commentItem = new CommentItem(layoutInflater, parent);
        commentItem.setCommentId(id);
        commentItem.setFocus();
        commentItem.addListener(listener);
        return commentItem;
    }
}


//MainActivity
public class AppCompatActivity implements CommentItem.Listener {


    private LinearLayout commentContainer;
    private final View rootView;
    private final LayoutInflater layoutInflater;

    private final List<Comment> comments = new ArrayList<>();

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        bindViews();
        addComment();
    }

    private void bindViews() {
        commentContainer = findViewById(R.id.comment_container);
    }


    @Override
    public void onCommentChanged(CommentItem item) {
        for (Comment comment : comments) {
            if (comment.getId() == item.getCommentId()) {
                comment.setContent(item.getText());
            }
        }
    }

    @Override
    public void onEnterPressed(CommentItem item) {
        if (comments.size() >= 5) {
            return;
        }

        addComment();
    }

    @Override
    public void onDeleteClicked(CommentItem item) {
        Comment toBeDeleted = item.toComment();
        comments.remove(toBeDeleted);
        updateUi();
    }

    private void addComment() {
        Comment comment = new Comment(comments.size(), "");
        comments.add(comment);
        updateUi();
    }

    private void updateUi() {
        commentContainer.removeAllViews();

        for (Comment comment : comments) {
            CommentItem commentItem = CommentItem.newCommentItem(layoutInflater, null, comment.getId(), this);
            commentItem.setText(comment.getContent());
            commentItem.addListener(this);
            commentContainer.addView(commentItem.getRootView());
        }
    }

}

 

커스텀 컴포넌트를 쓴다던지, CommentItem에 Comment instance를 포함한다던지 다양한 구현방법이 있을 수 있습니다. 짧은 시간 안에 테스트 했기 때문에, 데이터와 뷰를 어떻게 분리했는지에 포커스를 두시고, 제 코드를 그냥 복사 붙여넣기 하지는 마시기를 권장드립니다. 키 포인트는 Comment가 실제 데이터를 포함하고 있고 List<Comment>를 통해 뷰를 관리하는다는 점입니다.

spark (227,470 포인트) 님이 2021년 3월 7일 답변
감사합니다 선생님 이렇게까지ㅜㅜ..너무 많이 배우는것 같습니다..막상하라면 못할것같지만..그래서 지금 코드보고 하나하나 따라가면서 어떻게 되는지 보고있습니다..몇개는 저한테 맞게 약간만 수정하면서 진행했습니다.

아직 코드를 다보지는 않았지만

귀찮으시겠지만 질문몇가지만 드리겠습니다ㅠ


1)첫번째 질문에 대한 답변도 좀 가능하실까요? 선생님이 처음 올려주신 코드에 대한 질문인데 아직도 이해가 잘 가질 않아서..

2) 지금 올려주신 코드에 대해서 질문이 있는데,
액티비티코드를 처음에 진행해보니까 null 레퍼런스 에러가 나더라구요
updateUI에서 newCommentItem(layoutInflater, , ) 에서 에러가 났는데,
이유가 필드로 선언하신 layoutinflater 때문이더군요.
private final로 선언하셨는데 저 상태로하면 초기화가 안됐다고 컴파일러가 바로 에러를 띄웁니다..rootView도 마찬가지구요. 사실 액티비티의rootVIew 필드는 액티비티 내에서 시용되질않아 크게 상관없을것같지만..
제가 잘못된걸까요?

그래서 일단 rootiView랑 inflater 필드를 둘다 private으로만 바꿔주고
inflater는 bindView()에서 getLayoutinflater()를 호출해서 null 에러가 안나게 해줘서 진행을 했습니다. 제가 어디가 잘못된걸까요

3) newCommentItem()을 static으로 작성하셨던데 이게 단순히 id,그러니까 사이즈를 지속적으로 업데이트 해주기 위함인가요?

4)앞전 코드에서는 아이템 생성시 키보드가 자동 유지되었는데 이번에는 그렇지 않습니다. requestFocus에 의해 포커스는 되는데 자동 키보드는 올라오질 않아요.
EditText의 디폴트가 자동 키보드로 알고있는데..혹시나해서 android:windowSoftInputMode="stateAlwaysVisible" 이라던지 focusable,focusableInTouchMode 속성 다 건드려봤는데 자동 키보드는 안되는데 어떤 이유때문인지 아실까요?

감사합니다
1)첫번째 질문 -> 정확하게 어떤 질문을 말씀하시는 건지. 혹 레이아웃 문제이면 거면  디바이스에 따라 화면의 높이가 넘칠 수 있기 때문에 제일 바깥에 스클롤뷰를 무조건 쓰여야 합니다.

2) 해당 필드는 필요없구요. 액티비티에서는 getLayoutInflator()를 사용하시면 LayoutInflater에 접근하실 수 있습니다. 만약 프레그먼트라면 LayoutInflater.from(context)를 쓰시면 되구요. 잘 안되는 부분이 있으시면 개발자 문서에서 해당 클래스를 확인해 보셔서 어떤 필드와 메소드가 제공되는지 확인하셔야 합니다. 안그러면,  copy & paste를 할 수 밖에 없습니다.

3) 이건 팩토리 메소드라고 부르는 겁니다. 즉 생성자를 직접 호출하지 않고 필요한 파라미터를  세팅해서 호출하기 위해 넣은 거구요. CommentItem의 생성자를 private으로 만드시면 팩토리 메소드의 의도를 이해하실 수 있을겁니다. 이 클래스의 객체는 반드시 이 메소드를 통해서 강제하기 위한 의도이죠. 사실 이게 Effective Java라는 책에서 가능하면 사용하라는 패턴입니다.

4)이유는 EditText를 동적으로 생성되고 있기 때문에 그런겁니다. XML에 이미 EditText가 존재한다면 widnwowSoftInputMode가 동작을 할겁니다. 필요하시면 처음 아이템은 XML에 넣으셔도 되구요, 코드를 통해서 하실 거면, 시스템서비스를 이용해서 InputMethodManager 를 구해오신 다음에, show/hide 를 해주셔야 합니다. 아주 흔한 코드이기때문에 인터넷 검색하시면 아주 많이 검색됩니다.


말씀드렸다시피 제코드를 가져다 쓰시지는 마시고 제 코드에서 의도하는 바를 이해하시고 님의 요구사항에 맞게 코드를 만드시길 바랍니다.  몇가지 포인트만 짚어드리자면,
Comment라는 클래스가 주석의 정보를 저장하고 있습니다.
View자체 대한 로직은 CommentItem에 별도로 분리했습니다. 이 부분은 커스텀뷰를 쓰시거나  LayoutInflater로 inflate를 하신다음 FrameLayout에 addView하셔도  전혀 상관이 없습니다. RecyclerView의 ViewHolder쓰는 것과 결과적으로 비슷한 역할을 한다고 보시면 됩니다. View에서 일어나는 interaction 은 listener를 통해 처리하고 있습니다.
그리고 MainActivity에서는 List<Comment> 를 관리하고(추가, 수정, 삭제), 이 리스트에 따라 EditText를 생성해 주고 있습니다.
항상 코드를 작성해주시면 그대로 복사는 절대 안합니다. 복사만해서는 절대 코드를 이해를 못해서 한줄한줄 따라가면서 어떻게 돌아가는지 이해하려고 노력하고있습니다
감사합니다!!
...