Android
Создание пользовательских представлений
Поиск…
Создание пользовательских представлений
Если вам требуется полностью настроенное представление, вам нужно подклассировать View
(суперкласс всех Android-представлений) и предоставить свой собственный размер ( onMeasure(...)
) и onDraw(...)
рисования ( onDraw(...)
):
Создайте свой собственный скелет вида: это в основном то же самое для каждого пользовательского представления. Здесь мы создаем скелет для пользовательского представления, которое может нарисовать смайлик под названием
SmileyView
:public class SmileyView extends View { private Paint mCirclePaint; private Paint mEyeAndMouthPaint; private float mCenterX; private float mCenterY; private float mRadius; private RectF mArcBounds = new RectF(); public SmileyView(Context context) { this(context, null, 0); } public SmileyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SmileyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initPaints(); } private void initPaints() {/* ... */} @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {/* ... */} @Override protected void onDraw(Canvas canvas) {/* ... */} }
Инициализация ваших красок: объекты
Paint
- это кисти вашего виртуального холста, определяющие, как визуализируются ваши геометрические объекты (например, цвет, стиль заливки и штрихов и т. Д.). Здесь мы создаем двеPaint
s, одну желтую заполненную краску для круга и одну черную краску для глаз и рта:private void initPaints() { mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCirclePaint.setStyle(Paint.Style.FILL); mCirclePaint.setColor(Color.YELLOW); mEyeAndMouthPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mEyeAndMouthPaint.setStyle(Paint.Style.STROKE); mEyeAndMouthPaint.setStrokeWidth(16 * getResources().getDisplayMetrics().density); mEyeAndMouthPaint.setStrokeCap(Paint.Cap.ROUND); mEyeAndMouthPaint.setColor(Color.BLACK); }
Внедрите свой собственный
onMeasure(...)
: это необходимо, чтобы родительские макеты (например,FrameLayout
) могли правильно выровнять пользовательский вид. Она предоставляет наборmeasureSpecs
, которые можно использовать , чтобы определить высоту и ширину вашего вида. Здесь мы создаем квадрат, убедившись, что высота и ширина одинаковы:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); int h = MeasureSpec.getSize(heightMeasureSpec); int size = Math.min(w, h); setMeasuredDimension(size, size); }
Обратите внимание, что
onMeasure(...)
должен содержать хотя бы один вызовsetMeasuredDimension(..)
иначе ваше пользовательское представление будет сбой с помощьюIllegalStateException
.Внедрите собственный
onSizeChanged(...)
: это позволяет вамonSizeChanged(...)
текущую высоту и ширину вашего пользовательского представления для правильной настройки вашего кода рендеринга. Здесь мы просто вычислим наш центр и наш радиус:@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mCenterX = w / 2f; mCenterY = h / 2f; mRadius = Math.min(w, h) / 2f; }
Внедрите собственный
onDraw(...)
: здесь вы реализуете фактический рендеринг своего представления. Он предоставляет объектCanvas
который вы можете рисовать (см. Официальную документациюCanvas
для всех доступных доступных методов рисования).@Override protected void onDraw(Canvas canvas) { // draw face canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint); // draw eyes float eyeRadius = mRadius / 5f; float eyeOffsetX = mRadius / 3f; float eyeOffsetY = mRadius / 3f; canvas.drawCircle(mCenterX - eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint); canvas.drawCircle(mCenterX + eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint); // draw mouth float mouthInset = mRadius /3f; mArcBounds.set(mouthInset, mouthInset, mRadius * 2 - mouthInset, mRadius * 2 - mouthInset); canvas.drawArc(mArcBounds, 45f, 90f, false, mEyeAndMouthPaint); }
Добавьте свой собственный вид в макет: теперь пользовательский вид может быть включен в любые файлы макетов, которые у вас есть. Здесь мы просто обертываем его внутри
FrameLayout
:<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.app.SmileyView android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
Обратите внимание: рекомендуется, чтобы проект был создан после завершения кода представления. Без его создания вы не сможете увидеть представление на экране предварительного просмотра в Android Studio.
После того, как вы собрали все вместе, вы должны приветствовать следующий экран после запуска операции, содержащей вышеуказанный макет:
Добавление атрибутов в представления
Пользовательские представления также могут принимать пользовательские атрибуты, которые могут использоваться в файлах ресурсов макета Android. Чтобы добавить атрибуты в пользовательский вид, вам необходимо сделать следующее:
Определите имя и тип ваших атрибутов: это делается внутри
res/values/attrs.xml
(при необходимости создайте его). Следующий файл определяет атрибут цвета для цвета лица смайлика и атрибут enum для выражения смайлика:<resources> <declare-styleable name="SmileyView"> <attr name="smileyColor" format="color" /> <attr name="smileyExpression" format="enum"> <enum name="happy" value="0"/> <enum name="sad" value="1"/> </attr> </declare-styleable> <!-- attributes for other views --> </resources>
Используйте свои атрибуты внутри своего макета: это можно сделать в любых файлах макета, которые используют ваш пользовательский вид. Следующий файл макета создает экран со счастливым желтым смайликом:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="match_parent" android:layout_width="match_parent"> <com.example.app.SmileyView android:layout_height="56dp" android:layout_width="56dp" app:smileyColor="#ffff00" app:smileyExpression="happy" /> </FrameLayout>
Совет. Пользовательские атрибуты не работают с
tools:
префикс в Android Studio 2.1 и старше (и, возможно, в будущих версиях). В этом примере заменаapp:smileyColor
наtools:smileyColor
приведет кtools:smileyColor
чтоsmileyColor
не будет установлен во время выполнения или во время разработки.Прочтите свои атрибуты: это делается внутри вашего пользовательского исходного кода. Следующий фрагмент
SmileyView
демонстрирует, как можно извлечь атрибуты:public class SmileyView extends View { // ... public SmileyView(Context context) { this(context, null); } public SmileyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SmileyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmileyView, defStyleAttr, 0); mFaceColor = a.getColor(R.styleable.SmileyView_smileyColor, Color.TRANSPARENT); mFaceExpression = a.getInteger(R.styleable.SmileyView_smileyExpression, Expression.HAPPY); // Important: always recycle the TypedArray a.recycle(); // initPaints(); ... } }
(Необязательно) Добавить стиль по умолчанию: это делается путем добавления стиля со значениями по умолчанию и загрузки его в пользовательское представление. Следующий стиль смайлика по умолчанию представляет собой счастливый желтый цвет:
<!-- styles.xml --> <style name="DefaultSmileyStyle"> <item name="smileyColor">#ffff00</item> <item name="smileyExpression">happy</item> </style>
Что применяется в нашем
SmileyView
, добавив его в качестве последнего параметра вызова дляobtainStyledAttributes
(см. Код на шаге 3):TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmileyView, defStyleAttr, R.style.DefaultSmileyViewStyle);
Обратите внимание, что любые значения атрибутов, установленные в файле раздутого макета (см. Код на шаге 2), переопределяют соответствующие значения стиля по умолчанию.
(Необязательно). Предоставляйте стили внутри тем: это делается путем добавления нового атрибута ссылки на стиль, который можно использовать внутри ваших тем и предоставления стиля для этого атрибута. Здесь мы просто назовем наш ссылочный атрибут
smileyStyle
:<!-- attrs.xml --> <attr name="smileyStyle" format="reference" />
Затем мы предоставляем стиль для нашей темы приложения (здесь мы просто используем стиль по умолчанию с шага 4):
<!-- themes.xml --> <style name="AppTheme" parent="AppBaseTheme"> <item name="smileyStyle">@style/DefaultSmileyStyle</item> </style>
Создание сложного вида
ViewGroup
вид представляет собой настраиваемую ViewGroup
которая рассматривается как единое представление по окружающему программному коду. Такая ViewGroup может быть действительно полезна в DDD- подобном дизайне, поскольку она может соответствовать совокупности в этом примере Contact. Его можно повторно использовать везде, где отображается контакт.
Это означает, что окружающий код контроллера, Activity, Fragment или Adapter может просто передать объект данных в представление, не разделяя его на несколько различных виджетах пользовательского интерфейса.
Это облегчает повторное использование кода и обеспечивает лучшую конструкцию в соответствии с SOLID priciples .
Макет XML
Обычно вы начинаете. У вас есть существующий бит XML, который вы повторно используете, возможно, как <include/>
. Извлеките его в отдельный XML-файл и оберните корневой тег в элемент <merge>
:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/photo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentRight="true" />
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/photo" />
<TextView
android:id="@+id/phone_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_toLeftOf="@id/photo" />
</merge>
Этот XML-файл отлично работает в редакторе макетов в Android Studio. Вы можете рассматривать его как любой другой макет.
Составная группа ViewGroup
После того, как у вас есть файл XML, создайте настраиваемую группу представлений.
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.ImageView;
import android.widget.TextView;
import myapp.R;
/**
* A compound view to show contacts.
*
* This class can be put into an XML layout or instantiated programmatically, it
* will work correctly either way.
*/
public class ContactView extends RelativeLayout {
// This class extends RelativeLayout because that comes with an automatic
// (MATCH_PARENT, MATCH_PARENT) layout for its child item. You can extend
// the raw android.view.ViewGroup class if you want more control. See the
// note in the layout XML why you wouldn't want to extend a complex view
// such as RelativeLayout.
// 1. Implement superclass constructors.
public ContactView(Context context) {
super(context);
init(context, null);
}
// two extra constructors left out to keep the example shorter
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ContactView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
// 2. Initialize the view by inflating an XML using `this` as parent
private TextView mName;
private TextView mPhoneNumber;
private ImageView mPhoto;
private void init(Context context, AttributeSet attrs) {
LayoutInflater.from(context).inflate(R.layout.contact_view, this, true);
mName = (TextView) findViewById(R.id.name);
mPhoneNumber = (TextView) findViewById(R.id.phone_number);
mPhoto = (ImageView) findViewById(R.id.photo);
}
// 3. Define a setter that's expressed in your domain model. This is what the example is
// all about. All controller code can just invoke this setter instead of fiddling with
// lots of strings, visibility options, colors, animations, etc. If you don't use a
// custom view, this code will usually end up in a static helper method (bad) or copies
// of this code will be copy-pasted all over the place (worse).
public void setContact(Contact contact) {
mName.setText(contact.getName());
mPhoneNumber.setText(contact.getPhoneNumber());
if (contact.hasPhoto()) {
mPhoto.setVisibility(View.VISIBLE);
mPhoto.setImageBitmap(contact.getPhoto());
} else {
mPhoto.setVisibility(View.GONE);
}
}
}
Метод init(Context, AttributeSet)
- это то, где вы читали бы любые пользовательские атрибуты XML, как описано в разделе «Добавление атрибутов в представления» .
Используя эти штуки, вы можете использовать его в своем приложении.
Использование в XML
Вот пример fragment_contact_info.xml
который иллюстрирует, как вы поместили бы один ContactView поверх списка сообщений:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- The compound view becomes like any other view XML element -->
<myapp.ContactView
android:id="@+id/contact"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/message_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
Использование в кодексе
Вот пример RecyclerView.Adapter
который показывает список контактов. Этот пример иллюстрирует, насколько сильно становится код контроллера, когда он полностью свободен от манипуляций View.
package myapp;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
public class ContactsAdapter extends RecyclerView.Adapter<ContactsViewHolder> {
private final Context context;
public ContactsAdapter(final Context context) {
this.context = context;
}
@Override
public ContactsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ContactView v = new ContactView(context); // <--- this
return new ContactsViewHolder(v);
}
@Override
public void onBindViewHolder(ContactsViewHolder holder, int position) {
Contact contact = this.getItem(position);
holder.setContact(contact); // <--- this
}
static class ContactsViewHolder extends RecyclerView.ViewHolder {
public ContactsViewHolder(ContactView itemView) {
super(itemView);
}
public void setContact(Contact contact) {
((ContactView) itemView).setContact(contact); // <--- this
}
}
}
Советы по настройке CustomView
Не выделяйте новые объекты в onDraw
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(); //Do not allocate here
}
Вместо рисования чертежей в холсте ...
drawable.setBounds(boundsRect);
drawable.draw(canvas);
Используйте Bitmap для более быстрого рисования:
canvas.drawBitmap(bitmap, srcRect, boundsRect, paint);
Не перерисовывайте весь вид, чтобы обновить только небольшую часть. Вместо этого перерисуйте определенную часть представления.
invalidate(boundToBeRefreshed);
Если ваше представление делает некоторую непрерывную анимацию, например, часовую onStop()
показывающую каждую секунду, по крайней мере останавливает анимацию на onStop()
активности и запускает ее на onStart()
активности.
Не делайте никаких вычислений внутри метода onDraw
вида, вы должны закончить рисование перед вызовом invalidate()
. Используя эту технику, вы можете избежать падения кадров в вашем представлении.
Повороты
Основные операции представления - это перевод, поворот и т. Д. Практически каждый разработчик столкнулся с этой проблемой, когда они используют растровые или градиенты в своем пользовательском представлении. Если в представлении будет показано повернутое представление, и растровое изображение должно быть повернуто в этом пользовательском представлении, многие из нас подумают, что это будет дорого. Многие считают, что поворот растрового изображения очень дорог, потому что для этого вам нужно перевести матрицу пикселей растрового изображения. Но правда в том, что это не так сложно! Вместо поворота растрового изображения просто поверните сам холст!
// Save the canvas state
int save = canvas.save();
// Rotate the canvas by providing the center point as pivot and angle
canvas.rotate(pivotX, pivotY, angle);
// Draw whatever you want
// Basically whatever you draw here will be drawn as per the angle you rotated the canvas
canvas.drawBitmap(...);
// Now restore your your canvas to its original state
canvas.restore(save);
// Unless canvas is restored to its original state, further draw will also be rotated.
Соединение для SVG / VectorDrawable как drawableRight
Главным мотивом для разработки этого сложного представления является то, что ниже 5.0 устройств не поддерживает svg в drawable внутри TextView / EditText. Еще одно преимущество - мы можем установить height
и width
drawableRight
внутри EditText
. Я отделил его от моего проекта и создал в отдельном модуле.
Имя модуля: custom_edit_drawable (сокращенное имя для prefix-c_d_e)
Префикс «c_d_e_» для использования, чтобы ресурсы модуля приложения не должны переопределять их по ошибке. Пример: префикс «abc» используется Google в библиотеке поддержки.
build.gradle
dependencies {
compile 'com.android.support:appcompat-v7:25.3.1'
}
используйте AppCompat> = 23
Файл макета: c_e_d_compound_view.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/edt_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
android:paddingEnd="40dp"
android:paddingLeft="5dp"
android:paddingRight="40dp"
android:paddingStart="5dp" />
<!--make sure you are not using ImageView instead of this-->
<android.support.v7.widget.AppCompatImageView
android:id="@+id/drawbleRight_search"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="right|center_vertical"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp" />
</FrameLayout>
Пользовательские атрибуты: attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="EditTextWithDrawable">
<attr name="c_e_d_drawableRightSVG" format="reference" />
<attr name="c_e_d_hint" format="string" />
<attr name="c_e_d_textSize" format="dimension" />
<attr name="c_e_d_textColor" format="color" />
</declare-styleable>
</resources>
Код: EditTextWithDrawable.java
public class EditTextWithDrawable extends FrameLayout {
public AppCompatImageView mDrawableRight;
public EditText mEditText;
public EditTextWithDrawable(Context context) {
super(context);
init(null);
}
public EditTextWithDrawable(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}
private void init(AttributeSet attrs) {
if (attrs != null && !isInEditMode()) {
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.c_e_d_compound_view, this, true);
mDrawableRight = (AppCompatImageView) ((FrameLayout) getChildAt(0)).getChildAt(1);
mEditText = (EditText) ((FrameLayout) getChildAt(0)).getChildAt(0);
TypedArray attributeArray = getContext().obtainStyledAttributes(
attrs,
R.styleable.EditTextWithDrawable);
int drawableRes =
attributeArray.getResourceId(
R.styleable.EditTextWithDrawable_c_e_d_drawableRightSVG, -1);
if (drawableRes != -1) {
mDrawableRight.setImageResource(drawableRes);
}
mEditText.setHint(attributeArray.getString(
R.styleable.EditTextWithDrawable_c_e_d_hint));
mEditText.setTextColor(attributeArray.getColor(
R.styleable.EditTextWithDrawable_c_e_d_textColor, Color.BLACK));
int textSize = attributeArray.getDimensionPixelSize(R.styleable.EditTextWithDrawable_c_e_d_textSize, 15);
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
android.view.ViewGroup.LayoutParams layoutParams = mDrawableRight.getLayoutParams();
layoutParams.width = (textSize * 3) / 2;
layoutParams.height = (textSize * 3) / 2;
mDrawableRight.setLayoutParams(layoutParams);
attributeArray.recycle();
}
}
}
Пример: как использовать вид сверху
Макет: activity_main.xml
<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:orientation="vertical">
<com.customeditdrawable.AppEditTextWithDrawable
android:id="@+id/edt_search_emp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:c_e_d_drawableRightSVG="@drawable/ic_svg_search"
app:c_e_d_hint="@string/hint_search_here"
app:c_e_d_textColor="@color/text_color_dark_on_light_bg"
app:c_e_d_textSize="@dimen/text_size_small" />
</LinearLayout>
Деятельность: MainActivity.java
public class MainActivity extends AppCompatActivity {
EditTextWithDrawable mEditTextWithDrawable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEditTextWithDrawable= (EditTextWithDrawable) findViewById(R.id.edt_search_emp);
}
}
Реагирование на события Touch
Многие пользовательские представления должны принимать пользовательское взаимодействие в виде сенсорных событий. Вы можете получить доступ к событиям касания, переопределив onTouchEvent
. Существует ряд действий, которые вы можете отфильтровать. Основные из них:
-
ACTION_DOWN
: Это срабатывает один раз, когда ваш палец сначала касается вида. -
ACTION_MOVE
: Это называется каждый раз, когда ваш палец немного перемещается по представлению. Его называют много раз. -
ACTION_UP
: Это последнее действие, которое нужно вызывать, когда вы поднимаете палец с экрана.
Вы можете добавить следующий вид к вашему представлению, а затем наблюдать за выходом журнала, когда вы касаетесь и перемещаете свой палец вокруг своего вида.
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomView", "onTouchEvent: ACTION_DOWN: x = " + x + ", y = " + y);
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomView", "onTouchEvent: ACTION_MOVE: x = " + x + ", y = " + y);
break;
case MotionEvent.ACTION_UP:
Log.i("CustomView", "onTouchEvent: ACTION_UP: x = " + x + ", y = " + y);
break;
}
return true;
}
Дальнейшее чтение: