Android 사용자 정의: 원하는 것을 수행하는 UI 구성요소를 빌드하는 방법

게시 됨: 2022-03-11

개발자가 목표로 하는 플랫폼에서 제공하지 않거나 실제로 제공되지만 특정 속성이나 동작이 없는 UI 구성 요소가 필요한 경우가 종종 있습니다. 두 시나리오에 대한 답은 사용자 정의 UI 구성 요소입니다.

Android UI 모델은 본질적으로 사용자 정의가 가능하며 Android 사용자 정의, 테스트 및 다양한 방법으로 사용자 정의 UI 구성 요소를 생성하는 기능을 제공합니다.

  • 기존 구성 요소(예: TextView , ImageView 등)를 상속하고 필요한 기능을 추가/재정의합니다. 예를 들어, ImageView 를 상속하는 CircleImageView , 표시되는 이미지를 원으로 제한하기 위해 onDraw() 함수를 재정의하고, 외부 메모리에서 이미지를 로드하기 위해 loadFromFile() 함수를 추가합니다.

  • 여러 구성 요소에서 복합 구성 요소를 만듭니다 . 이 접근 방식은 일반적으로 레이아웃을 활용하여 구성 요소가 화면에 배열되는 방식을 제어합니다. 예를 들어 수평 방향의 LinearLayout 을 상속하고 레이블 역할을 하는 TextView 와 텍스트 입력 필드 역할을 하는 EditText 를 모두 포함하는 LabeledEditText 입니다.

    이 접근 방식은 이전 방식을 사용할 수도 있습니다. 즉, 내부 구성 요소는 기본 또는 사용자 정의일 수 있습니다.

  • 가장 다양하고 가장 복잡한 접근 방식은 자체 드로잉 구성 요소를 만드는 것 입니다. 이 경우 구성 요소는 일반 View 클래스를 상속하고 레이아웃을 결정하기 위해 onMeasure() , 내용을 표시하기 위해 onDraw() 와 같은 함수를 재정의합니다. 이 방법으로 생성된 구성 요소는 일반적으로 Android의 2D 그리기 API에 크게 의존합니다.

Android 사용자 지정 사례 연구: CalendarView

Android는 기본 CalendarView 구성 요소를 제공합니다. 그것은 잘 수행되고 모든 달력 구성 요소에서 예상되는 최소한의 기능을 제공하여 한 달 전체를 표시하고 현재 날짜를 강조 표시합니다. 어떤 사람들은 그것이 좋아 보인다고 말할 수도 있지만 네이티브 모양을 원하고 모양을 사용자 정의하는 데 관심이 없는 경우에만 해당됩니다.

예를 들어 CalendarView 구성 요소는 특정 날짜가 표시되는 방식이나 사용할 배경색을 변경할 수 있는 방법을 제공하지 않습니다. 예를 들어 특별한 날을 표시하기 위해 사용자 정의 텍스트나 그래픽을 추가할 방법도 없습니다. 간단히 말해서 구성 요소는 다음과 같으며 거의 ​​아무것도 변경할 수 없습니다.

스크린샷
AppCompact.Light 테마의 CalendarView .

당신 만의 것을 만드세요

그렇다면 자신만의 캘린더 보기를 만드는 방법은 무엇입니까? 위의 모든 접근 방식이 작동합니다. 그러나 실용성은 일반적으로 세 번째 옵션(2D 그래픽)을 배제하고 두 가지 다른 방법을 남겨두므로 이 기사에서는 이 두 가지를 혼합하여 사용할 것입니다.

따라하기 위해 여기에서 소스 코드를 찾을 수 있습니다.

1. 컴포넌트 레이아웃

먼저 구성 요소가 어떻게 보이는지부터 시작하겠습니다. 간단하게 하기 위해 그리드에 날짜를 표시하고 상단에 "다음 달" 및 "이전 달" 버튼과 함께 월 이름을 표시하겠습니다.

스크린샷
사용자 정의 캘린더 보기.

이 레이아웃은 다음과 같이 control_calendar.xml 파일에 정의되어 있습니다. 일부 반복적인 마크업은 ... 로 축약되었습니다.

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>

2. 구성 요소 클래스

이전 레이아웃은 Activity 또는 Fragment 에 있는 그대로 포함될 수 있으며 제대로 작동합니다. 그러나 독립 실행형 UI 구성 요소로 캡슐화하면 코드 반복을 방지하고 각 모듈이 하나의 책임을 처리하는 모듈식 설계를 허용합니다.

우리의 UI 구성 요소는 XML 레이아웃 파일의 루트와 일치하는 LinearLayout 이 될 것입니다. 코드에서 중요한 부분만 표시된다는 점에 유의하십시오. 구성 요소의 구현은 CalendarView.java 에 있습니다.

 public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }

코드는 매우 간단합니다. 생성 시 구성 요소는 XML 레이아웃을 확장하고 완료되면 나중에 더 쉽게 액세스할 수 있도록 내부 컨트롤을 로컬 변수에 할당합니다.

3. 약간의 논리가 필요합니다

이 구성 요소가 실제로 달력 보기로 작동하도록 하려면 몇 가지 비즈니스 논리가 필요합니다. 처음에는 복잡해 보일 수 있지만 실제로는 그다지 많지 않습니다. 분해해 보겠습니다.

  1. 달력 보기의 너비는 7일이며 모든 월은 첫 번째 행의 어딘가에서 시작됩니다.

  2. 먼저 월이 시작하는 위치를 파악한 다음 그 이전의 모든 위치를 0 위치에 도달할 때까지 이전 달의 숫자(30, 29, 28 등)로 채워야 합니다.

  3. 그런 다음 이번 달의 날짜를 채웁니다(1, 2, 3… 등).

  4. 그 후 다음 달의 날(다시 1, 2, 3.. 등)이 왔지만 이번에는 그리드의 마지막 행에 있는 나머지 위치만 채웁니다.

다음 다이어그램은 이러한 단계를 보여줍니다.

스크린샷
사용자 정의 캘린더 보기 비즈니스 로직.

그리드의 너비는 이미 주간 달력을 나타내는 7개의 셀로 지정되어 있지만 높이는 어떻습니까? 그리드의 가장 큰 크기는 첫 번째 행의 마지막 셀인 토요일에 시작하는 31일 월의 최악의 시나리오에 의해 결정될 수 있으며 전체를 표시하려면 5개의 행이 더 필요합니다. 따라서 6개의 행(총 42일)을 표시하도록 달력을 설정하면 모든 경우를 처리하기에 충분합니다.

그러나 모든 달에 31일이 있는 것은 아닙니다! Android에 내장된 날짜 기능을 사용하여 날짜 수를 직접 계산할 필요가 없도록 하여 이로 인해 발생하는 복잡한 문제를 피할 수 있습니다.

앞에서 언급했듯이 Calendar 클래스에서 제공하는 날짜 기능은 구현을 매우 간단하게 만듭니다. 구성 요소에서 updateCalendar() 함수는 다음 논리를 구현합니다.

 private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }

4. 마음에 드는 커스터마이징

개별 날짜 표시를 담당하는 구성 요소는 GridView 이므로 날짜 표시 방법을 사용자 지정하는 좋은 위치는 Adapter 입니다. 어댑터는 개별 그리드 셀에 대한 데이터를 보유하고 뷰를 확장하는 역할을 하기 때문입니다.

이 예에서는 CalendearView 에서 다음이 필요합니다.

  • 현재 날짜는 굵은 파란색 텍스트 여야 합니다.
  • 당월 이외의 날은 회색 으로 표시되어야 합니다.
  • 이벤트가 있는 날에는 특별한 아이콘이 표시되어야 합니다.
  • 캘린더 헤더는 계절(여름, 가을, 겨울, 봄)에 따라 색상이 변경되어야 합니다.

처음 세 가지 요구 사항은 텍스트 속성과 배경 리소스를 변경하여 간단하게 달성할 수 있습니다. 이 작업을 수행하기 위해 CalendarAdapter 를 구현해 보겠습니다. CalendarView 의 멤버 클래스가 될 수 있을 만큼 간단합니다. getView() 함수를 재정의하여 위의 요구 사항을 달성할 수 있습니다.

 @Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }

최종 설계 요구 사항은 좀 더 많은 작업이 필요합니다. 먼저 /res/values/colors.xml 에 사계절 색상을 추가해 보겠습니다.

 <color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>

그런 다음 배열을 사용하여 월별 계절을 정의해 보겠습니다(간단하게 북반구로 가정합니다. 죄송합니다. 호주!). CalendarView 에서 다음 멤버 변수를 추가합니다.

 // seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};

이런 식으로 적절한 색상 선택은 적절한 계절( monthSeason[currentMonth] )을 선택한 다음 해당 색상( rainbow[monthSeason[currentMonth] )을 선택하여 수행되며 적절한 색상이 선택되었는지 확인하기 위해 updateCalendar() 에 추가됩니다. 달력이 변경될 때마다.

 // set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));

이를 통해 다음과 같은 결과를 얻습니다.

안드로이드 커스터마이징
헤더 색상은 계절에 따라 변경됩니다.

중요 참고 사항 HashSet 이 객체를 비교하는 방식으로 인해 위의 updateCalendar() )에서 eventDays.contains(date) 확인은 날짜 객체가 정확히 동일하지 않는 한 날짜 객체에 대해 true를 생성하지 않습니다. Date 데이터 유형에 대한 특별한 검사는 수행하지 않습니다. 이 문제를 해결하기 위해 이 검사는 다음 코드로 대체됩니다.

 for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }

5. 디자인 타임에 보기 흉하다

디자인 타임에 자리 표시자에 대한 Android의 선택은 의심스러울 수 있습니다. 다행히 Android는 UI 디자이너에서 렌더링하기 위해 실제로 구성 요소를 인스턴스화하고 구성 요소 생성자에서 updateCalendar() 를 호출하여 이를 악용할 수 있습니다. 이런 식으로 구성 요소는 실제로 디자인 타임에 의미가 있습니다.

스크린샷

구성 요소를 초기화할 때 많은 처리가 필요하거나 많은 데이터를 로드하는 경우 IDE의 성능에 영향을 줄 수 있습니다. 이 경우 Android는 UI 디자이너에서 구성 요소가 실제로 인스턴스화될 때 사용되는 데이터의 양을 제한하는 데 사용할 수 있는 isInEditMode() 라는 멋진 함수를 제공합니다. 예를 들어 CalendarView 에 로드할 이벤트가 많은 경우 updateCalendar() 함수 내에서 isInEditMode() 를 사용하여 디자인 모드에서 비어 있거나 제한된 이벤트 목록을 제공하고 그렇지 않으면 실제 목록을 로드할 수 있습니다.

6. 구성 요소 호출

구성 요소는 XML 레이아웃 파일에 포함될 수 있습니다(샘플 사용법은 activity_main.xml 에서 찾을 수 있음):

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

레이아웃이 로드되면 상호 작용하기 위해 검색됩니다.

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

위의 코드는 이벤트의 HashSet 을 만들고 여기에 현재 날짜를 추가한 다음 CalendarView 에 전달합니다. 결과적으로 CalendarView 는 현재 날짜를 굵은 파란색으로 표시하고 이벤트 마커도 표시합니다.

스크린샷
이벤트를 표시하는 CalendarView

7. 속성 추가

Android에서 제공하는 또 다른 기능은 사용자 지정 구성 요소에 속성을 할당하는 것입니다. 이를 통해 구성 요소를 사용하는 Android 개발자는 레이아웃 XML을 통해 설정을 선택하고 런타임에 CalendarView 가 어떻게 보이는지 기다려야 하는 대신 UI 디자이너에서 즉시 결과를 볼 수 있습니다. 구성 요소의 날짜 형식 표시를 변경하는 기능을 추가해 보겠습니다. 예를 들어 세 글자로 된 약어 대신 월의 전체 이름을 철자하는 것과 같은 기능을 추가해 보겠습니다.

이렇게 하려면 다음 단계가 필요합니다.

  • 속성을 선언합니다. 이것을 dateFormat 이라고 부르고 string 데이터 유형을 지정해 보겠습니다. /res/values/attrs.xml 에 추가하십시오.
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • 구성 요소를 사용하는 레이아웃에서 속성을 사용하고 "MMMM yyyy" 값을 지정합니다.
 <samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
  • 마지막으로 구성 요소가 속성 값을 사용하도록 합니다.
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] 프로젝트를 빌드하면 UI 디자이너에 표시된 날짜가 "2015년 7월"과 같이 해당 월의 전체 이름을 사용하도록 변경되었음을 알 수 있습니다. 다른 값을 제공하고 무슨 일이 일어나는지 보십시오.

스크린샷
CalendarView 속성 변경.

8. 구성 요소와 상호 작용

특정 요일에 눌러보셨나요? 구성 요소의 내부 UI 요소는 여전히 정상적인 예상 방식으로 작동하며 사용자 작업에 대한 응답으로 이벤트를 발생시킵니다. 그렇다면 이러한 이벤트를 어떻게 처리해야 할까요?

대답에는 두 부분이 포함됩니다.

  • 구성 요소 내부의 이벤트를 캡처하고
  • 구성 요소의 부모( Fragment , Activity 또는 다른 구성 요소일 수 있음)에 이벤트를 보고합니다.

첫 번째 부분은 매우 간단합니다. 예를 들어, 길게 누르는 그리드 항목을 처리하기 위해 구성 요소 클래스에 해당 리스너를 할당합니다.

 // long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });

이벤트를 보고하는 방법에는 여러 가지가 있습니다. 직접적이고 간단한 방법은 Android가 수행하는 방식을 복사하는 것입니다. 구성 요소의 부모(위 코드 조각의 eventHandler )에 의해 구현되는 구성 요소의 이벤트에 대한 인터페이스를 제공합니다.

인터페이스의 기능은 애플리케이션과 관련된 모든 데이터를 전달할 수 있습니다. 우리의 경우 인터페이스는 눌린 날짜의 날짜가 전달되는 하나의 이벤트 핸들러를 노출해야 합니다. CalendarView 에는 다음 인터페이스가 정의되어 있습니다.

 public interface EventHandler { void onDayLongPress(Date date); }

부모가 제공하는 구현은 setEventHandler() 를 통해 캘린더 보기에 제공될 수 있습니다. 다음은 `MainActivity.java'의 샘플 사용법입니다.

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }

하루를 길게 누르면 GridView 에서 캡처 및 처리하고 제공된 구현에서 onDayLongPress() 를 호출하여 보고하는 길게 누르기 이벤트가 발생합니다. 그러면 누른 날짜가 화면에 표시됩니다.

스크린샷

이를 처리하는 또 다른 고급 방법은 Android의 IntentsBroadcastReceivers 를 사용하는 것입니다. 이는 여러 구성 요소가 캘린더의 이벤트에 대해 알림을 받아야 할 때 특히 유용합니다. 예를 들어 달력에서 날짜를 누르면 Activity 에 표시할 텍스트와 백그라운드 Service 에서 다운로드할 파일이 필요합니다.

이전 접근 방식을 사용하려면 Activity 가 구성 요소에 EventHandler 를 제공하고 이벤트를 처리한 다음 Service 에 전달해야 합니다. 대신 구성 요소가 Intent 를 브로드캐스트하도록 하고 ActivityService 모두가 Intent를 자체 BroadcastReceivers 를 통해 이를 수락하도록 하면 삶이 더 쉬워질 뿐만 아니라 문제의 ActivityService 를 분리하는 데 도움이 됩니다.

결론

Android 사용자 정의의 놀라운 힘을 확인하십시오!
트위터

따라서 다음은 몇 가지 간단한 단계로 사용자 정의 구성 요소를 만드는 방법입니다.

  • XML 레이아웃을 만들고 필요에 맞게 스타일을 지정합니다.
  • XML 레이아웃에 따라 적절한 상위 구성 요소에서 구성 요소 클래스를 파생시킵니다.
  • 구성 요소의 비즈니스 논리를 추가합니다.
  • 속성을 사용하여 사용자가 구성 요소의 동작을 수정할 수 있도록 합니다.
  • UI 디자이너에서 구성 요소를 더 쉽게 사용하려면 Android의 isInEditMode() 함수를 사용하세요.

이 기사에서는 주식 달력 보기가 여러 면에서 부족하기 때문에 주로 달력 보기를 예로 만들었습니다. 그러나 만들 수 있는 구성 요소의 종류에는 제한이 없습니다. 동일한 기술을 사용하여 필요한 모든 것을 만들 수 있습니다. 하늘이 한계입니다!

이 가이드를 읽어주셔서 감사합니다. 코딩 작업에서 행운을 빕니다!