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 oknie tworzenia wiadomości.

Jeśli chcesz wyświetlić obrazy statyczne w aplikacji, do rysowania kształtów i obrazów możesz użyć klasy Drawable i jej podklas. Drawable to ogólna abstrakcja obiektu, co można narysować. Różne podklasy pomagają w określonych sytuacjach związanych z obrazami. Możesz je rozszerzyć, aby zdefiniować własne rysowalne obiekty, które zachowują się w unikalny sposób.

Oprócz korzystania z konstruktorów klas istnieją 2 sposoby definiowania i tworzenia instancji obiektu Drawable:

  • Dodaj zasób obrazu (plik bitmapy) zapisany w projekcie.
  • Utwórz zasób XML, który określa właściwości, które można przeciągnąć.

Uwaga: zamiast tego możesz korzystać z elementu rysowalnego w formie wektorowej, który definiuje obraz z zestawem punktów, linii i krzywych wraz z powiązanymi informacjami o kolorze. Umożliwia to skalowanie elementów rysunkowych wektorowych w przypadku różnych rozmiarów bez utraty jakości. Więcej informacji znajdziesz w artykule Omówienie elementów rysunkowych wektorowych.

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

Aby dodać grafikę do aplikacji, możesz utworzyć odwołanie do pliku graficznego z zasobów projektu. Obsługiwane typy plików to PNG (preferowany), JPG (akceptowany) i GIF (odradzane). Do tej techniki dobrze nadają się ikony aplikacji, logo i inne elementy graficzne, takie jak używane w grach.

Aby użyć zasobu obrazu, dodaj plik do katalogu res/drawable/ swojego projektu. W projekcie możesz odwoływać się do zasobu obrazu w kodzie lub w układzie XML. W obu przypadkach jest tu używany identyfikator zasobu, który jest nazwą pliku bez rozszerzenia typu pliku. Na przykład określ my_image.png jako my_image.

Uwaga: zasoby obrazów umieszczone w katalogu res/drawable/ mogą być automatycznie optymalizowane przez narzędzie aapt podczas kompilacji za pomocą bezstratnej kompresji obrazów. Na przykład plik PNG o prawdziwych kolorach, który nie wymaga więcej niż 256 kolorów, może zostać przekonwertowany do 8-bitowego pliku PNG z paletą kolorów. Dzięki temu otrzymujemy obraz o jednakowej jakości, ale wymaga on mniej pamięci. W związku z tym pliki binarne umieszczone w tym katalogu mogą się zmieniać podczas kompilacji. Jeśli zamierzasz odczytać obraz jako strumień bitów, aby przekonwertować go na bitmapę, umieść obrazy w folderze res/raw/, gdzie narzędzie aapt ich nie zmieni.

Ten fragment kodu pokazuje, jak utworzyć obiekt ImageView, który korzysta z obrazu utworzonego z zasobów rysowalnych 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 chcieć traktować zasób graficzny 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 jeden stan, niezależnie od tego, ile różnych obiektów go dla niego zainicjujesz. Jeśli na przykład utworzysz wystąpienie 2 obiektów Drawable z tego samego zasobu obrazu i zmienisz właściwość (np. alfa) jednego z nich, wpłynie to również na drugi obiekt. Gdy mamy do czynienia z wieloma wystąpieniami zasobu graficznego, zamiast bezpośrednio przekształcać obiekt Drawable, należy użyć animacji młodszej.

Poniższy fragment kodu XML pokazuje, jak dodać rysowalny zasób 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 używasz zasobów graficznych jako źródła elementów rysowanych, upewnij się, że mają one odpowiedni rozmiar przy różnej gęstości pikseli. Jeśli obrazy są niepoprawne, zostaną powiększone, by pasowały, co może spowodować powstanie artefaktów w elementach rysowalnych. Więcej informacji znajdziesz w artykule Obsługa różnych gęstości pikseli.

Tworzenie elementów rysowalnych na podstawie zasobów XML

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

Po zdefiniowaniu pliku Drawable w formacie XML zapisz go w katalogu res/drawable/ swojego projektu. Poniższy przykład przedstawia kod XML definiujący zasób TransitionDrawable, który dziedziczy z pola 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 Resources#getDrawable() i przekazując identyfikator zasobu pliku XML. Każda podklasa Drawable obsługująca metodę inflate() może być zdefiniowana w pliku XML, a jej instancja zostanie utworzona przez aplikację.

Każda rysowalna klasa, która obsługuje inflację kodu XML, korzysta z konkretnych atrybutów XML, które pomagają zdefiniować właściwości obiektów. 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 wymienionych powyżej klasach.

Kształty elementów rysowanych

Obiekt ShapeDrawable może być dobrym rozwiązaniem, gdy chcesz dynamicznie rysować dwuwymiarową grafikę. Możesz programowo rysować podstawowe kształty w obiekcie ShapeDrawable i stosować style potrzebne Twojej aplikacji.

ShapeDrawable jest podklasą klasy Drawable. Z tego względu elementu ShapeDrawable można używać wszędzie tam, gdzie oczekiwana jest wartość Drawable. Możesz na przykład użyć obiektu ShapeDrawable do ustawienia tła 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 rysuje obiekt ShapeDrawable podczas zdarzenia onDraw(), jak widać 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);
  }
}

Możesz użyć klasy CustomDrawableView w przykładowym kodzie powyżej tak jak dowolnego innego widoku niestandardowego. Możesz na przykład automatycznie dodać ją do działania w aplikacji, jak pokazano 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 chcesz użyć widoku niestandardowego w układzie XML, klasa CustomDrawableView musi zastąpić konstruktor View(Context, AttributeSet), który jest wywoływany, gdy klasa jest uzupełniana z kodu XML. Ten przykład pokazuje, jak zadeklarować właściwość 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, które można rysować w pakiecie android.graphics.drawable, pozwala definiować różne właściwości obiektu przy użyciu metod publicznych. Możesz dostosować np. przezroczystość alfa, filtr kolorów, dicie, nieprzezroczystość i kolor.

Możesz też definiować podstawowe kształty, które można rysować, korzystając z zasobów XML. Więcej informacji znajdziesz w sekcji Kształt, który można rysować w artykule Typy zasobów rysowalnych.

Elementy rysowalne NinePatch

Grafika NinePatchDrawable to rozciągnięty obraz bitmapowy, którego można używać jako tła widoku. Android automatycznie zmienia rozmiar grafiki, aby dopasować ją do zawartości widoku. Przykładem wykorzystania obrazu NinePatch jest tło używane przez standardowe przyciski na Androidzie – przyciski muszą się rozciągać, aby pomieścić ciągi 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.

Obramowanie służy do definiowania rozciągniętych i statycznych obszarów obrazu. Sekcja, którą można rozciągać, wskazujesz, rysując jedną (lub więcej) czarną linię o szerokości 1 piksela po 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, które można rozciągać. Względny rozmiar sekcji elastycznych pozostaje taki sam, więc największa z nich pozostaje zawsze największa.

Możesz też zdefiniować opcjonalną rysowaną sekcję obrazu (w efekcie linie dopełnienia), rysując linię po prawej stronie i linię u dołu. Jeśli obiekt View ustawia grafikę NinePatch jako tło, a następnie określa tekst widoku, rozciąga się on tak, że cały tekst zajmuje tylko obszar wyznaczony przez linię prawą i dolną (jeśli jest podany). Jeśli nie podasz linii dopełnienia, Android użyje linii lewej i górnej do zdefiniowania tego rysowalnego obszaru.

Aby ułatwić zrozumienie różnicy między liniami, linia lewa i górna określają, które piksele obrazu mogą być replikowane w celu jego rozciągnięcia. Linie dolne i po prawej określają względny obszar obrazu, który może zajmować zawartość widoku.

Rysunek 1 przedstawia przykładową grafikę NinePatch służącą do definiowania przycisku:

Obraz obszaru rozciągniętego
i pola dopełnienia

Rysunek 1. Przykład grafiki NinePatch, która określa przycisk

Ta grafika NinePatch definiuje jeden elastyczny obszar z linią lewą i górną oraz obszar rysowalny – z linią dolną i prawą. Na obrazie u góry szare przerywane linie identyfikują replikowane obszary obrazu w celu jego rozciągnięcia. Różowy prostokąt na dolnym obrazie wskazuje obszar, w którym dozwolona jest zawartość widoku. Jeśli treść nie mieści się w tym obszarze, obraz jest rozciągnięty, aby się zmieścił.

Narzędzie Draw 9-patch umożliwia tworzenie obrazów NinePatch w edytorze grafiki WYSIWYG. Ostrzeżenia pojawiają się nawet wtedy, gdy w wyniku replikacji piksela w obszarze zdefiniowanym dla obszaru rozciągniętego mogą pojawić się artefakty rysowania.

Poniższy przykładowy kod XML układu pokazuje, jak dodać grafikę NinePatch do kilku przycisków. Obraz NinePatch jest zapisywany w folderze 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, aby przycisk pasował do tekstu.

Rysunek 2 przedstawia 2 przyciski renderowane z przedstawionych powyżej obrazów XML i NinePatch. Zwróć uwagę, że szerokość i wysokość przycisku różnią się w zależności od tekstu, a obraz tła jest rozciągany, aby dopasować się do niego.

Obraz malutkich,
normalnych przycisków

Rysunek 2. Przyciski renderowane z wykorzystaniem zasobu XML i grafiki NinePatch

Niestandardowe elementy rysowane

Jeśli chcesz tworzyć rysunki niestandardowe, możesz to zrobić, rozszerzając klasę Drawable (lub dowolną jej podklasy).

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

Poniższy kod przedstawia 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;
    }
}

Następnie możesz dodać obiekt do rysowania, gdziekolwiek chcesz, np. w elemencie ImageView, jak pokazano tutaj:

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 w wersji 7.0 (poziom interfejsu API 24) i nowszych możesz też definiować wystąpienia niestandardowego elementu rysowalnego za pomocą kodu XML w następujący sposób:

  • Użycie pełnej i jednoznacznej nazwy klasy jako nazwy elementu XML. W przypadku tego podejścia niestandardowa klasa, którą można przeciągnąć, 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 określenie w pełni kwalifikowanej nazwy klasy z atrybutu class. Tej metody można używać 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 rysowanych

W Androidzie 5.0 (poziom interfejsu API 21) lub nowszym możesz zabarwić mapy bitowe i 9 poprawek zdefiniowanych jako maski alfa. Możesz zabarwić je za pomocą zasobów kolorów lub atrybutów motywu, które łączą zasoby kolorów (np. ?android:attr/colorPrimary). Zwykle tworzy się te zasoby tylko raz i automatycznie koloruje się, aby dopasować je do motywu.

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

Wyodrębnianie widocznych kolorów z obrazu

Biblioteka pomocy Androida zawiera klasę Palette, która umożliwia wyodrębnianie wyraźnych kolorów z obrazu. Możesz wczytać elementy do rysowania jako obiekty Bitmap i przekazać je do aplikacji Palette, aby uzyskać dostęp do jej kolorów. Więcej informacji znajdziesz w artykule Wybieranie kolorów za pomocą interfejsu Palette API.