Omówienie elementów rysunkowych

Wypróbuj sposób tworzenia wiadomości
Jetpack Compose to zalecany zestaw narzędzi UI na Androida. Dowiedz się, jak wyświetlać grafikę w funkcji Compose

Jeśli musisz wyświetlić w aplikacji statyczne obrazy, możesz rysować kształty i obrazy za pomocą klasy Drawable i jej podklas. Drawable to ogólna abstrakcja dla elementu, który można narysować. Różne podklasy pomagają w określonych scenariuszach tworzenia obrazów. Można je też rozszerzać, aby definiować własne obiekty rysowalne, które zachowują się w unikalny sposób.

Poza użyciem konstruktorów klas są jeszcze 2 sposoby definiowania i tworzenia instancji Drawable:

  • Powiększ zasób graficzny (plik bitmapy) zapisany w projekcie.
  • Dodaj komponent XML, który określa właściwości możliwe do rysowania.

Uwaga: lepiej jest używać rysowania wektorowego, które definiuje obraz z zestawem punktów, linii i krzywych oraz powiązanych informacji o kolorze. Dzięki temu obiekty rysowane wektorowo można skalować do różnych rozmiarów bez utraty jakości. Więcej informacji znajdziesz w artykule Omówienie elementów rysowalnych wektorowych.

Tworzenie elementów rysunkowych na podstawie obrazów zasobów

Możesz dodać grafikę do swojej aplikacji, odwołując się do pliku graficznego z zasobów projektu. Obsługiwane typy plików to PNG (preferowane), JPG (akceptowane) i GIF (zalecane). Do tej techniki dobrze nadają się ikony aplikacji, logo i inne elementy graficzne, np. używane w grach.

Aby użyć zasobu graficznego, dodaj plik do katalogu res/drawable/ projektu. W projekcie możesz odwoływać się do zasobu obrazu w kodzie lub układzie XML. W obu przypadkach korzysta się z identyfikatora zasobu, czyli nazwy pliku bez rozszerzenia typu pliku. Na przykład określ my_image.png jako my_image.

Uwaga: zasoby graficzne umieszczone w katalogu res/drawable/ mogą być automatycznie optymalizowane przy użyciu bezstratnej kompresji obrazów przez narzędzie aapt podczas procesu kompilacji. Na przykład plik PNG z prawdziwymi kolorami, który nie wymaga więcej niż 256 kolorów, może zostać przekonwertowany na 8-bitowy PNG z paletą kolorów. W ten sposób można uzyskać obraz o równej jakości, ale zużywa mniej pamięci. W efekcie pliki binarne obrazów umieszczone w tym katalogu mogą się zmieniać podczas kompilacji. Jeśli planujesz odczytywać obraz jako strumień bitowy, aby przekonwertować go na bitmapę, umieść obrazy w folderze res/raw/, w którym narzędzie aapt ich nie modyfikuje.

Ten fragment kodu pokazuje, jak utworzyć obiekt ImageView, który używa obrazu utworzonego z zasobu możliwego do rysowania i dodaje go do układu:

Kotlin

private lateinit var constraintLayout: ConstraintLayout

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Instantiate an ImageView and define its properties
    val i = ImageView(this).apply {
        setImageResource(R.drawable.my_image)
        contentDescription = resources.getString(R.string.my_image_desc)

        // set the ImageView bounds to match the Drawable's dimensions
        adjustViewBounds = true
        layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT)
    }

    // Create a ConstraintLayout in which to add the ImageView
    constraintLayout = ConstraintLayout(this).apply {

        // Add the ImageView to the layout.
        addView(i)
    }

    // Set the layout as the content view.
    setContentView(constraintLayout)
}

Java

ConstraintLayout constraintLayout;

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  // Create a ConstraintLayout in which to add the ImageView
  constraintLayout = new ConstraintLayout(this);

  // Instantiate an ImageView and define its properties
  ImageView i = new ImageView(this);
  i.setImageResource(R.drawable.my_image);
  i.setContentDescription(getResources().getString(R.string.my_image_desc));

  // set the ImageView bounds to match the Drawable's dimensions
  i.setAdjustViewBounds(true);
  i.setLayoutParams(new ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT));

  // Add the ImageView to the layout and set the layout as the content view.
  constraintLayout.addView(i);
  setContentView(constraintLayout);
}

W innych przypadkach możesz obsługiwać zasób obrazu jako obiekt Drawable, jak w tym przykładzie:

Kotlin

val myImage: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, null)

Java

Resources res = context.getResources();
Drawable myImage = ResourcesCompat.getDrawable(res, R.drawable.my_image, null);

Ostrzeżenie: każdy unikalny zasób w projekcie może utrzymywać tylko 1 stan, niezależnie od tego, ile różnych obiektów dla niego inicjujesz. Jeśli np. utworzysz instancję 2 obiektów Drawable z tego samego zasobu obrazu i zmienisz właściwość (np. alfa) jednego z obiektów, będzie to miało wpływ na drugi obiekt. Jeśli pracujesz z wieloma instancjami zasobu graficznego, zamiast bezpośrednio przekształcać obiekt Drawable, wykonaj animację pośrednią.

Poniższy fragment kodu XML pokazuje, jak dodać zasób rysowalny do elementu ImageView w układzie XML:

<ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/my_image"
        android:contentDescription="@string/my_image_desc" />

Więcej informacji o korzystaniu z zasobów projektu znajdziesz w artykule Zasoby.

Uwaga: jeśli jako źródła elementów rysowanych używasz zasobów graficznych, upewnij się, że mają one rozmiar odpowiedni do różnych gęstości pikseli. Jeśli obrazy nie będą poprawne, zostaną powiększone, by pasowały, co może skutkować pojawieniem się artefaktów w elementach rysowanych. Więcej informacji znajdziesz w artykule Obsługa różnych gęstości pikseli.

Tworzenie elementów rysunkowych na podstawie zasobów XML

Jeśli chcesz utworzyć obiekt Drawable, który początkowo nie jest zależny od zmiennych zdefiniowanych w Twoim kodzie ani od interakcji użytkownika, dobrym rozwiązaniem będzie zdefiniowanie obiektu Drawable w pliku XML. Nawet jeśli spodziewasz się, że Drawable zmieni swoje właściwości podczas interakcji użytkownika z aplikacją, rozważ zdefiniowanie obiektu w pliku XML, ponieważ właściwości można modyfikować po utworzeniu obiektu.

Po zdefiniowaniu pliku Drawable w formacie XML zapisz plik w katalogu res/drawable/ projektu. Poniższy przykład pokazuje kod XML definiujący zasób TransitionDrawable, który dziedziczy z Drawable:

<!-- res/drawable/expand_collapse.xml -->
<transition xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/image_expand"/>
    <item android:drawable="@drawable/image_collapse"/>
</transition>

Następnie pobierz i utwórz instancję obiektu, wywołując funkcję Resources#getDrawable() i przekazując identyfikator zasobu pliku XML. Każdą podklasę Drawable, która obsługuje metodę inflate(), można zdefiniować w pliku XML i utworzyć jej instancję w aplikacji.

Każda klasa rysowalna, która obsługuje inflację kodu XML, wykorzystuje określone atrybuty XML, które ułatwiają definiowanie właściwości obiektu. Ten kod tworzy instancję TransitionDrawable i ustawia ją jako zawartość obiektu ImageView:

Kotlin

val transition= ResourcesCompat.getDrawable(
        context.resources,
        R.drawable.expand_collapse,
        null
) as TransitionDrawable

val image: ImageView = findViewById(R.id.toggle_image)
image.setImageDrawable(transition)

// Description of the initial state that the drawable represents.
image.contentDescription = resources.getString(R.string.collapsed)

// Then you can call the TransitionDrawable object's methods.
transition.startTransition(1000)

// After the transition is complete, change the image's content description
// to reflect the new state.

Java

Resources res = context.getResources();
TransitionDrawable transition =
    (TransitionDrawable) ResourcesCompat.getDrawable(res, R.drawable.expand_collapse, null);

ImageView image = (ImageView) findViewById(R.id.toggle_image);
image.setImageDrawable(transition);

// Description of the initial state that the drawable represents.
image.setContentDescription(getResources().getString(R.string.collapsed));

// Then you can call the TransitionDrawable object's methods.
transition.startTransition(1000);

// After the transition is complete, change the image's content description
// to reflect the new state.

Więcej informacji o obsługiwanych atrybutach XML znajdziesz w klasach wymienionych powyżej.

Elementy rysowane kształtami

Jeśli chcesz dynamicznie rysować dwuwymiarową grafikę, możesz skorzystać z obiektu ShapeDrawable. Możesz programowo rysować kształty podstawowe w obiekcie ShapeDrawable i stosować style, których potrzebuje Twoja aplikacja.

ShapeDrawable jest podklasą klasy Drawable. Dlatego możesz używać ShapeDrawable wszędzie tam, gdzie ma być oczekiwana wartość Drawable. Możesz na przykład użyć obiektu ShapeDrawable, aby ustawić tło widoku, przekazując je do metody setBackgroundDrawable() widoku. Możesz też narysować kształt jako własny widok niestandardowy i dodać go do układu w aplikacji.

ShapeDrawable ma własną metodę draw(), więc możesz utworzyć podklasę View, która będzie pobierać obiekt ShapeDrawable podczas zdarzenia onDraw(), jak pokazano w tym przykładowym kodzie:

Kotlin

class CustomDrawableView(context: Context) : View(context) {
    private val drawable: ShapeDrawable = run {
        val x = 10
        val y = 10
        val width = 300
        val height = 50
        contentDescription = context.resources.getString(R.string.my_view_desc)

        ShapeDrawable(OvalShape()).apply {
            // If the color isn't set, the shape uses black as the default.
            paint.color = 0xff74AC23.toInt()
            // If the bounds aren't set, the shape can't be drawn.
            setBounds(x, y, x + width, y + height)
        }
    }

    override fun onDraw(canvas: Canvas) {
        drawable.draw(canvas)
    }
}

Java

public class CustomDrawableView extends View {
  private ShapeDrawable drawable;

  public CustomDrawableView(Context context) {
    super(context);

    int x = 10;
    int y = 10;
    int width = 300;
    int height = 50;
    setContentDescription(context.getResources().getString(
            R.string.my_view_desc));

    drawable = new ShapeDrawable(new OvalShape());
    // If the color isn't set, the shape uses black as the default.
    drawable.getPaint().setColor(0xff74AC23);
    // If the bounds aren't set, the shape can't be drawn.
    drawable.setBounds(x, y, x + width, y + height);
  }

  protected void onDraw(Canvas canvas) {
    drawable.draw(canvas);
  }
}

klasy CustomDrawableView z przykładowego kodu powyżej możesz użyć tak samo jak dowolnego innego widoku niestandardowego. Można na przykład automatycznie dodać go do aktywności w aplikacji, jak w tym przykładzie:

Kotlin

private lateinit var customDrawableView: CustomDrawableView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    customDrawableView = CustomDrawableView(this)

    setContentView(customDrawableView)
}

Java

CustomDrawableView customDrawableView;

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  customDrawableView = new CustomDrawableView(this);

  setContentView(customDrawableView);
}

Jeśli w układzie XML chcesz użyć widoku niestandardowego, klasa CustomDrawableView musi zastąpić konstruktor View(Context, AttributeSet), który jest wywoływany, gdy klasa zostanie wzbogacona z pliku XML. Poniższy przykład pokazuje, jak zadeklarować CustomDrawableView w układzie XML:

<com.example.shapedrawable.CustomDrawableView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />

Klasa ShapeDrawable, podobnie jak wiele innych typów rysowalnych w pakiecie android.graphics.drawable, umożliwia definiowanie różnych właściwości obiektu za pomocą metod publicznych. Przykładowe właściwości, które można dostosować, to przezroczystość (alfa), filtr kolorów, zmiana koloru, nieprzezroczystość i kolor.

Możesz też definiować podstawowe kształty rysowane za pomocą zasobów XML. Więcej informacji znajdziesz w sekcji Kształt rysowalny w artykule o typach zasobów rysowalnych.

Elementy rysowane NinePatch

Grafika NinePatchDrawable to rozciągana mapa bitowa, której możesz użyć jako tła widoku. Android automatycznie zmienia rozmiar grafiki, aby dostosować ją do zawartości widoku. Przykładem użycia obrazu NinePatch jest tło stosowane w standardowych przyciskach Androida – przyciski muszą być rozciągane, aby dopasować je do ciągów o różnej długości. Grafika NinePatch to standardowy obraz PNG z dodatkowym ramką o szerokości 1 piksela. Musi zostać zapisany z rozszerzeniem 9.png w katalogu res/drawable/ projektu.

Użyj obramowania, aby określić rozciągane i statyczne obszary obrazu. Aby wskazać sekcję rozciąganą, rysuj co najmniej jedną czarną linię o szerokości 1 piksela w lewej i górnej części obramowania (pozostałe piksele obramowania powinny być w pełni przezroczyste lub białe). Możesz mieć dowolną liczbę sekcji rozciąganych. Względny rozmiar sekcji rozciąganych pozostaje taki sam, więc największa pozostaje zawsze największa.

Możesz też zdefiniować opcjonalną rysowaną część obrazu (czyli linie dopełnienia), rysując linię po prawej stronie i linię na dole. Jeśli obiekt View ustawia grafikę NinePatch jako tło, a następnie określa tekst widoku, rozciąga się on w taki sposób, że cały tekst zajmuje tylko obszar wyznaczony przez prawą i dolną linię (jeśli są uwzględnione). Jeśli nie masz linii dopełnienia, Android określa ten rysowalny obszar na podstawie linii lewej i górnej.

Linie lewe i górne określają, które piksele obrazu mogą być replikowane w celu rozciągnięcia obrazu. Linie dolne i prawe określają względny obszar obrazu, który może zajmować zawartość widoku.

Ilustracja 1 pokazuje przykład grafiki NinePatch użytej do zdefiniowania przycisku:

Obraz rozciąganego obszaru
i ramki

Rysunek 1. Przykład grafiki NinePatch, która definiuje przycisk

Grafika NinePatch definiuje jeden rozciągany obszar z lewą i górną linią oraz obszar, który można rysować z linią dolną i prawą. Na górnym obrazie szare kropkowane linie wskazują obszary obrazu, które są replikowane w celu rozciągnięcia obrazu. Różowy prostokąt na obrazku u dołu wskazuje obszar, w którym jest dozwolona zawartość widoku. Jeśli zawartość nie mieści się w tym obszarze, obraz zostanie rozciągnięty, aby je dopasować.

Narzędzie Draw 9-patch to bardzo przydatny sposób tworzenia obrazów NinePatch za pomocą edytora grafiki WYSIWYG. Pojawia się nawet ostrzeżenie, jeśli w wyniku replikacji pikseli w regionie zdefiniowanym dla obszaru rozciąganego istnieje ryzyko, że w wyniku replikacji pikseli mogą powstać artefakty rysowania.

Poniższy przykładowy kod XML układu pokazuje, jak dodać grafikę NinePatch do kilku przycisków. Obraz NinePatch został zapisany na koncie res/drawable/my_button_background.9.png.

<Button android:id="@+id/tiny"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerInParent="true"
        android:text="Tiny"
        android:textSize="8sp"
        android:background="@drawable/my_button_background"/>

<Button android:id="@+id/big"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="true"
        android:text="Biiiiiiig text!"
        android:textSize="30sp"
        android:background="@drawable/my_button_background"/>

Pamiętaj, że atrybuty layout_width i layout_height mają wartość wrap_content, by przycisk dobrze pasował do tekstu.

Rysunek 2 przedstawia 2 przyciski renderowane z obrazów XML i NinePatch przedstawionych powyżej. Zwróć uwagę, że szerokość i wysokość przycisku zmienia się w zależności od tekstu, a obraz tła jest rozciągnięty, aby go zmieścić.

Obraz maleńkich
przycisków w normalnym rozmiarze

Rysunek 2. Przyciski renderowane za pomocą zasobu XML i grafiki NinePatch

Niestandardowe elementy rysowane

Aby utworzyć własne rysunki, możesz to zrobić, rozszerzając klasę Drawable (lub dowolną jej podklasę).

Najważniejszą metodą jest draw(Canvas), bo jest to obiekt Canvas, którego musisz użyć do przekazania instrukcji rysowania.

Ten kod pokazuje prostą podklasę klasy Drawable, która rysuje okrąg:

Kotlin

class MyDrawable : Drawable() {
    private val redPaint: Paint = Paint().apply { setARGB(255, 255, 0, 0) }

    override fun draw(canvas: Canvas) {
        // Get the drawable's bounds
        val width: Int = bounds.width()
        val height: Int = bounds.height()
        val radius: Float = Math.min(width, height).toFloat() / 2f

        // Draw a red circle in the center
        canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, redPaint)
    }

    override fun setAlpha(alpha: Int) {
        // This method is required
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        // This method is required
    }

    override fun getOpacity(): Int =
        // Must be PixelFormat.UNKNOWN, TRANSLUCENT, TRANSPARENT, or OPAQUE
        PixelFormat.OPAQUE
}

Java

public class MyDrawable extends Drawable {
    private final Paint redPaint;

    public MyDrawable() {
        // Set up color and text size
        redPaint = new Paint();
        redPaint.setARGB(255, 255, 0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
        // Get the drawable's bounds
        int width = getBounds().width();
        int height = getBounds().height();
        float radius = Math.min(width, height) / 2;

        // Draw a red circle in the center
        canvas.drawCircle(width/2, height/2, radius, redPaint);
    }

    @Override
    public void setAlpha(int alpha) {
        // This method is required
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        // This method is required
    }

    @Override
    public int getOpacity() {
        // Must be PixelFormat.UNKNOWN, TRANSLUCENT, TRANSPARENT, or OPAQUE
        return PixelFormat.OPAQUE;
    }
}

Potem możesz dodać element rysowalny w dowolnym miejscu, np. na ImageView w następujący sposób:

Kotlin

val myDrawing = MyDrawable()
val image: ImageView = findViewById(R.id.imageView)
image.setImageDrawable(myDrawing)
image.contentDescription = resources.getString(R.string.my_image_desc)

Java

MyDrawable mydrawing = new MyDrawable();
ImageView image = findViewById(R.id.imageView);
image.setImageDrawable(mydrawing);
image.setContentDescription(getResources().getString(R.string.my_image_desc));

W Androidzie 7.0 (poziom interfejsu API 24) i nowszych możesz również definiować wystąpienia niestandardowego obiektu rysowanego za pomocą XML w następujący sposób:

  • Używanie pełnej i jednoznacznej nazwy klasy jako nazwy elementu XML. W tym przypadku niestandardowa klasa rysowalna musi być publiczną klasą najwyższego poziomu:
    <com.myapp.MyDrawable xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="#ffff0000" />
    
  • użycie drawable jako nazwy tagu XML i podanie pełnej i jednoznacznej nazwy klasy z atrybutu class. Tej metody można użyć zarówno w przypadku publicznych klas najwyższego poziomu, jak i publicznych statycznych klas wewnętrznych:
    <drawable xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.myapp.MyTopLevelClass$MyDrawable"
        android:color="#ffff0000" />
    

Dodaj odcień do elementów, które można rysować

W Androidzie 5.0 (poziom interfejsu API 21) i nowszych można zabarwić mapy bitowe i 9 poprawek zdefiniowanych jako maski alfa. Możesz zastosować odcienie do zasobów kolorów lub atrybutów motywu, które są przypisane do zasobów kolorów (np. ?android:attr/colorPrimary). Zwykle zasoby te tworzysz tylko raz i automatycznie kolorujesz zgodnie z motywem.

Za pomocą metody setTint() możesz zastosować odcień do obiektów BitmapDrawable, NinePatchDrawable lub VectorDrawable. Za pomocą atrybutów android:tint i android:tintMode możesz też ustawić kolor i tryb odcienia w układach.

Wyodrębnij z obrazu najważniejsze kolory

Biblioteka pomocy Androida zawiera klasę Palette, która umożliwia wyodrębnianie z obrazu dobrze widocznych kolorów. Możesz załadować elementy rysowane jako obiekt Bitmap i przekazać je usłudze Palette, aby uzyskać dostęp do kolorów. Więcej informacji znajdziesz w artykule Wybieranie kolorów w interfejsie Palette API.