<나인패치>

이미지가 나타나는 영역보다 원본 이미지가 작으면 시스템이 이미지 크기를 자동으로 늘려준다. 이 과정에서 왜곡이 발생하는데, 이 왜곡을 해결하는 방법을 나인패치라고 한다.

 

나인(nine)패치 이미지는 파일 확장자 앞에 '.9'가 붙어야 한다. (ex. ABC.9.png)

 

아래 레포지토리에 들어가서 button_image 파일 2개를 다운받는다.

 

DoItAndroidRev7/part2/chapter07/SampleNinePatch/app/src/main/res/drawable at master · mike-jung/DoItAndroidRev7

Do it! 안드로이드 앱 프로그래밍(개정7판)의 소스 코드. Contribute to mike-jung/DoItAndroidRev7 development by creating an account on GitHub.

github.com

파일을 drawable 폴더에 넣어준 뒤, 레이아웃에 버튼 6개를 추가한다.

상위 3개 버튼은 일반 이미지로, 하위 3개 버튼은 나인패치 이미지로 background를 설정해준다.

그 다음 버튼의 width를 wrap_content로 설정해준 뒤 텍스트의 길이를 각각 다르게 설정하여 이미지가 깨지는지 확인한다.

나인패치 이미지는 멀쩡하지만, 일반 이미지로 설정한 상단의 버튼 3개는 이미지가 깨진 것을 확인할 수 있었다.

 

 

<새로운 뷰 만들기>

API에서 제공하는 뷰를 사용해서 새로운 뷰를 정의할 수 있다.

API에서 제공하는 뷰를 사용하려면 API의 뷰를 상속해야한다.

 

개발자가 뷰의 상태에 따라 추가적인 코드를 넣을 수 있도록 콜백 메서드가 호출된다.

  • onMeasure() : 뷰가 스스로의 크기를 정할 때 자동으로 호출됨. (폭과 높이 값이 파라미터로 전달됨)
  • setMeasuredDimension() : 뷰의 크기 값을 반환하고 싶을 때 사용하는 메서드.
  • onDraw() : 뷰가 화면에 보일 때 자동으로 호출됨. 만약 새로운 뷰를 클래스로 적용한 뒤 onDraw()를 재정의하면 다른 모양으로 보이는 뷰를 만들 수 있는 것이다. 새로 정의한 뷰는 뷰가 화면에 보이기 전에 호출된다.
  • invalidate() : 이 메서드를 호출하면 onDraw() 메서드가 다시 호출되기 때문에 뷰의 그래픽을 다시 그릴 수 있다.      (ex. 손가락으로 화면에 있는 뷰를 터치하여 이동시키려고 할 때 invalidate()를 호출하여 이동한 좌표에서 뷰의 그래픽을 다시 그리게 만들 수 있다.)

1. MainActivity가 위치한 폴더에 새로운 클래스 파일을 만든다. (MyButton)

 

2. MyButton.java 클래스를 작성한다.

AppCompatButton을 extends 했기 때문에 2개의 생성자를 필수로 추가해주어야한다.

생성자 추가 (우클릭 -> Generate -> Constructor)

package com.example.chapter7_2;

public class MyButton extends AppCompatButton {
    // 첫번째 생성자 : 이 뷰를 소스 코드에서 new 연산자로 생성하는 경우에 사용됨.
    public MyButton(@NonNull Context context) {
        super(context);
        init(context);
    }

    // 두번째 생성자 : 이 뷰를 XML 레이아웃에 추가하는 경우에 사용됨.
    public MyButton(@NonNull Context context, @Nullable AttributeSet attrs) {
        // AttributeSet 객체 : XML 레이아웃에서 태그에 추가하는 속성을 전달받기 위한 것
        super(context, attrs);
        init(context);
    }

    private void init(Context context) { // 뷰의 배경색과 글자색 설정
        setBackgroundColor(Color.CYAN);
        setTextColor(Color.BLACK);

        float textSize = getResources().getDimension(R.dimen.text_size);
        setTextSize(textSize);
        /* setTextSize에 바로 값을 넣을 경우 픽셀 단위 설정만 가능함.
        그래서 dimens.xml에 sp 단위의 크기 값을 정의한 다음, getDimension으로 가져오면 픽셀값으로 자동 변환된
        값을 넣을 수 있다. (sp 말고도 dp 등 다른 단위의 크기 값을 정의할 수 있다.)
         */
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d("MyButton","onDraw() 호출됨");
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("MyButton","onTouchEvent() 호출됨");

        int action = event.getAction();

        switch(action){
            case MotionEvent.ACTION_DOWN: // 버튼이 눌린 경우
                setBackgroundColor(Color.BLUE);
                setTextColor(Color.RED);
                break;

            case MotionEvent.ACTION_OUTSIDE:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                setBackgroundColor(Color.CYAN);
                setTextColor(Color.BLACK);
                break;
        }

        invalidate();
        return super.onTouchEvent(event);
    }
}

 

  • getDimension
        float textSize = getResources().getDimension(R.dimen.text_size);
        setTextSize(textSize);

여기서 getDiemension()의 파라미터로 sp 단위의 값을 넣어주기 위해 dimen 파일을 만들어준다.

res/values 폴더 내부에 dimens.xml을 만든다.

 

<dimens.xml>

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="text_size">16sp</dimen>
</resources>

 

3. activity_main.xml

Design 탭에서 최상위 레이아웃을 RelativeLayout으로 바꾼다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <!--직접 정의한 위젯이므로 XML 레이아웃에 추가할 때 패키지 이름까지 함께 넣어야 한다.-->
    <com.example.chapter7_2.MyButton
        android:id="@+id/button"
        android:layout_width="200dp"
        android:layout_height="80dp"
        android:layout_centerInParent="true"
        android:text="시작하기" />
    <!--상대 레이아웃에서는 layout_centerInParent 속성 값을 true로 하여 화면 가운데에 배치.-->
</RelativeLayout>

Button을 Design 탭에서 드래그로 추가한 뒤, Code 탭에서 <패키지경로.패키지명 으로 바꿔서 작성한다.

 

4. 실행 결과

버튼이 눌리지 않은 경우(기본 상태)
버튼이 눌린 경우 (누르고 있는 상태에서만)

 

<카드뷰>

카드뷰 : 프로필과 같은 간단 정보를 넣기 위해 각 영역을 구분하는 역할을 하는 뷰.

위에서 했던 새로운 뷰 만들기 방식으로 카드뷰를 만들 것이다.

 

1. 새 레이아웃 xml 파일, 자바 클래스 파일을 생성한다. (layout1.xml, Layout1.java)

 

2. layout1.xml

카드뷰를 넣어서 이런 느낌으로 만들어준다.

이미지뷰에 들어갈 이미지는 Mip Map의 ic_launcher를 넣어준다.

코드

더보기

 <CardView> 태그 부분을 한번 보고 넘어가자.

<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:orientation="horizontal">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardCornerRadius="10dp"
        app:cardElevation="5dp"
        app:cardUseCompatPadding="true">
        <!-- cardCornerRadius : 모서리를 둥글게 만듦 -->
        <!-- cardElevation : 뷰가 올라온 느낌이 들게 만듦 -->
        <!-- cardUseCompatPadding : 기본 패딩이 적용되도록 함. (true로 설정)-->

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/imageView"
                android:layout_width="80dp"
                android:layout_height="80dp"
                android:layout_marginRight="5dp"
                app:srcCompat="@mipmap/ic_launcher" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/textView"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="이름"
                    android:textSize="30sp" />

                <TextView
                    android:id="@+id/textView2"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="전화번호"
                    android:textColor="#3C5BD0"
                    android:textSize="25sp" />
            </LinearLayout>
        </LinearLayout>

    </androidx.cardview.widget.CardView>

</LinearLayout>
  • cardCornerRadius : 모서리를 둥글게 만듦
  • cardElevation : 뷰가 올라온 느낌이 들게 만듦
  • cardUseCompatPadding : 기본 패딩이 적용되도록 함. (true로 설정)

3. Layout1.java

LinearLayout을 상속받도록 작성한다.

Generate -> Constructor로 필수생성자를 2개 생성한다.

코드 간략 설명

  • 2개의 생성자에 각각 init() 메서드를 실행하는 코드를 추가한다.
  • init() 메서드를 작성한다. 인플레이션을 진행한 후, layout1.xml의 이미지뷰와 텍스트뷰를 참조한다.
  • setImage(), setName(), setMobile() 이렇게 3개의 메서드를 만든다.
package com.example.chapter7_3;

public class Layout1 extends LinearLayout {
    ImageView imageView;
    TextView textView1;
    TextView textView2;

    public Layout1(Context context) {
        super(context);
        init(context);
    }

    public Layout1(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context){
        // 인플레이션 진행하기
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.layout1, this, true);

        // 인플레이션 과정이 끝나면 XML 레이아웃 안에 있는 뷰를 참조할 수 있다.
        imageView = findViewById(R.id.imageView);
        textView1 = findViewById(R.id.textView);
        textView2 = findViewById(R.id.textView2);
    }

    public void setImage(int resId){ // resId는 resourceId이다.
        imageView.setImageResource(resId);
        // setImageResource() : 이미지뷰에 보이는 이미지를 바꿀 수 있는 메서드 중의 하나.
    }

    public void setName (String name){
        textView1.setText(name);
    }

    public void setMoblie (String mobile){
        textView2.setText(mobile);
    }
}

 

init()

더보기
private void init(Context context){
   // 인플레이션 진행하기
   LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   inflater.inflate(R.layout.layout1, this, true);

   // 인플레이션 과정이 끝나면 XML 레이아웃 안에 있는 뷰를 참조할 수 있다.
   imageView = findViewById(R.id.imageView);
   textView1 = findViewById(R.id.textView);
   textView2 = findViewById(R.id.textView2);
}

 

LayoutInflater 객체는 시스템 서비스로 제공되므로 getSystemService 메서드를 호출하면서 파라미터로 Context.LAYOUT_INFLATER_SERVICE 상수를 전달하면 객체가 반환된다.

4. activity_main.xml

아래와 같이 버튼 2개와 위에서 만든 layout1을 배치한다. (경로.이름)

더보기
<?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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="첫번째 이미지"
        android:textSize="20sp" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="두번째 이미지"
        android:textSize="20sp" />

    <com.example.chapter7_3.Layout1
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/layout1"
        />
</LinearLayout>

 

5. MainActivity.java

더보기

간단하다. 기본 이미지, 이름, 전화번호가 세팅되고 버튼을 누르면 이미지가 바뀐다.

package com.example.chapter7_3;

public class MainActivity extends AppCompatActivity {

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

        Layout1 layout1 = findViewById(R.id.layout1);

        layout1.setImage(R.drawable.ic_launcher_foreground);
        layout1.setName("강예지");
        layout1.setMoblie("010-1234-5678");

        Button button1 = findViewById(R.id.button);
        Button button2 = findViewById(R.id.button2);

        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                layout1.setImage(R.drawable.images);
            }
        });

        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                layout1.setImage(R.drawable.images2);
            }
        });
    }
}

 

6. 실행결과

초기화면(왼쪽) / '첫번째 이미지' 버튼 클릭 (중앙) / '두번째 이미지' 버튼 클릭 (오른쪽)