Gdy chcesz wyświetlać w aplikacji obrazy statyczne, możesz użyć klasy Drawable i jej podklas do rysowania kształtów i obrazów. Drawable to ogólna abstrakcja czegoś, co można narysować. Różne podklasy pomagają w określonych scenariuszach związanych z obrazami. Możesz je rozszerzać, aby definiować własne obiekty rysowalne, które zachowują się w unikalny sposób.
Oprócz konstruktorów klas istnieją 2 sposoby definiowania i tworzenia instancji klasy Drawable:
- Rozwiń zasób obrazu (plik bitmapy) zapisany w projekcie.
- Rozwiń zasób XML, który definiuje właściwości elementu rysowalnego.
Uwaga: możesz zamiast tego użyć obiektu rysowalnego wektorowo, który definiuje obraz za pomocą zestawu punktów, linii i krzywych wraz z powiązanymi informacjami o kolorach. Dzięki temu rysunki wektorowe można skalować do różnych rozmiarów bez utraty jakości. Więcej informacji znajdziesz w omówieniu rysunków wektorowych.
Tworzenie elementów rysowalnych z obrazów zasobów
Możesz dodać do aplikacji grafikę, odwołując się do pliku obrazu z zasobów projektu. Obsługiwane typy plików to PNG (preferowany), JPG (akceptowalny) i GIF (niezalecany). Ikony aplikacji, logo i inne elementy graficzne, np. te używane w grach, dobrze nadają się do tej techniki.
Aby użyć zasobu obrazu, dodaj plik do katalogu res/drawable/ projektu. Po dodaniu obrazu do projektu możesz odwołać się do niego w kodzie lub układzie XML. W obu przypadkach odwołuje się do niego identyfikator zasobu, czyli nazwa pliku bez rozszerzenia. Na przykład my_image.png to my_image.
Uwaga: zasoby obrazów umieszczone w katalogu res/drawable/ mogą być automatycznie optymalizowane za pomocą bezstratnej kompresji obrazów przez narzędzie aapt podczas procesu kompilacji. Na przykład obraz PNG w pełnej gamie kolorów, który nie wymaga więcej niż 256 kolorów, może zostać przekonwertowany na 8-bitowy obraz PNG z paletą kolorów. W ten sposób uzyskasz obraz o takiej samej jakości, ale wymagający mniej pamięci. W związku z tym pliki binarne obrazów umieszczone w tym katalogu mogą się zmieniać w czasie kompilacji. Jeśli planujesz odczytać obraz jako strumień bitów, 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 obiektu rysowalnego 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 obrazu jako obiekt Drawable, jak pokazano 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 mieć tylko jeden stan, niezależnie od tego, ile różnych obiektów dla niego utworzysz. Jeśli na przykład utworzysz 2 obiekty Drawable z tego samego zasobu obrazu i zmienisz właściwość (np. wartość alfa) jednego z nich, wpłynie to również na drugi obiekt. W przypadku wielu instancji zasobu obrazu zamiast bezpośrednio przekształcać obiekt Drawable, należy wykonać animację przejścia.
Fragment kodu XML poniżej pokazuje, jak dodać obiekt 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 i komponenty.
Uwaga: jeśli używasz zasobów obrazów jako źródła elementów rysowalnych, upewnij się, że obrazy mają odpowiedni rozmiar dla różnych gęstości pikseli. Jeśli obrazy nie są prawidłowe, zostaną powiększone, aby się zmieścić, co może spowodować artefakty w zasobach do rysowania. Więcej informacji znajdziesz w artykule Obsługa różnych gęstości pikseli.
Tworzenie elementów rysowalnych z zasobów XML
Jeśli chcesz utworzyć obiekt Drawable, który początkowo nie jest zależny od zmiennych zdefiniowanych przez kod lub interakcję użytkownika, zdefiniowanie obiektu Drawable w XML jest dobrym rozwiązaniem. Nawet jeśli spodziewasz się, że właściwości obiektu Drawable zmienią się podczas interakcji użytkownika z aplikacją, warto zdefiniować obiekt w XML, ponieważ po utworzeniu instancji obiektu możesz modyfikować jego właściwości.
Po zdefiniowaniu Drawable w XML zapisz plik w katalogu res/drawable/ projektu. Poniższy przykład pokazuje kod XML, który definiuje zasób TransitionDrawable dziedziczący po 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. W pliku XML można zdefiniować dowolną podklasę Drawable, która obsługuje metodę inflate(), i utworzyć jej instancję w aplikacji.
Każda klasa rysowalna, która obsługuje rozszerzanie XML, korzysta z określonych atrybutów XML, które pomagają zdefiniować właściwości obiektu. Poniższy 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.
Obiekty rysowalne kształtów
Obiekt ShapeDrawable może być dobrym rozwiązaniem, gdy chcesz dynamicznie rysować grafikę dwuwymiarową. Możesz programowo rysować kształty podstawowe na obiekcie ShapeDrawable i stosować style, których potrzebuje Twoja aplikacja.
ShapeDrawable jest podklasą klasy Drawable. Dlatego możesz użyć ShapeDrawable wszędzie tam, gdzie oczekiwana jest wartość Drawable. Możesz na przykład użyć obiektu ShapeDrawable, aby ustawić tło widoku, przekazując go do metody setBackgroundDrawable() widoku. Możesz też narysować kształt jako własny widok niestandardowy i dodać go do układu w aplikacji.
Ponieważ klasa ShapeDrawable ma własną metodę draw(), możesz utworzyć podklasę klasy View, która rysuje obiekt ShapeDrawable podczas zdarzenia onDraw(), jak pokazano w tym przykładzie kodu:
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 w przykładowym kodzie powyżej możesz używać tak jak każdego innego widoku niestandardowego. Możesz na przykład dodać go programowo do aktywności 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ć niestandardowego widoku w układzie XML, klasa CustomDrawableView musi zastąpić konstruktor View(Context, AttributeSet), który jest wywoływany, gdy klasa jest rozwijana z kodu XML. Poniższy przykład pokazuje, jak zadeklarować element 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 obiektów rysowalnych w pakiecie android.graphics.drawable, umożliwia definiowanie różnych właściwości obiektu za pomocą metod publicznych. Możesz dostosować m.in. te właściwości: przezroczystość alfa, filtr kolorów, dithering, krycie i kolor.
Możesz też definiować proste kształty rysowane za pomocą zasobów XML. Więcej informacji znajdziesz w artykule Shape drawable w sekcji Typy obiektów rysowalnych.
Obiekty rysowalne NinePatch
NinePatchDrawable to rozciągliwy obraz bitmapowy, którego możesz użyć jako tła widoku. Android automatycznie zmienia rozmiar grafiki, aby dopasować ją do zawartości widoku. Przykładem zastosowania obrazu NinePatch jest tło używane przez standardowe przyciski Androida – przyciski muszą się rozciągać, aby pomieścić ciągi znaków o różnej długości. Grafika NinePatch to standardowy obraz PNG z dodatkowym obramowaniem o grubości 1 piksela.
Musi być zapisany z rozszerzeniem 9.png w katalogu res/drawable/ projektu.
Użyj obramowania, aby określić rozciągliwe i statyczne obszary obrazu. Rozciągliwą sekcję oznaczysz, rysując 1-pikselową czarną linię (lub kilka takich linii) w lewej i górnej części ramki (pozostałe piksele ramki powinny być całkowicie przezroczyste lub białe). Możesz mieć dowolną liczbę sekcji rozciągliwych. Względny rozmiar rozciągliwych sekcji pozostaje taki sam, więc największa sekcja zawsze pozostaje największa.
Możesz też zdefiniować opcjonalną sekcję obrazu, którą można rysować (w zasadzie linie dopełniające), rysując linię po prawej stronie i linię u dołu. Jeśli obiektView ustawi grafikę NinePatch jako tło, a następnie określi tekst widoku, rozciągnie się tak, aby cały tekst zajmował tylko obszar wyznaczony przez prawą i dolną linię (jeśli są uwzględnione).
Jeśli linie dopełnienia nie są uwzględnione, Android używa linii po lewej i u góry, aby zdefiniować ten obszar rysowania.
Aby wyjaśnić różnicę między liniami, lewa i górna linia określają, które piksele obrazu można powielać, aby rozciągnąć obraz. Linie dolna i prawa określają względny obszar w obrazie, który mogą zajmować treści widoku.
Ilustracja 1 przedstawia przykład grafiki NinePatch użytej do zdefiniowania przycisku:
Rysunek 1. Przykład grafiki NinePatch, która definiuje przycisk
Grafika NinePatch określa jeden rozciągliwy obszar za pomocą linii po lewej i u góry oraz obszar rysowania za pomocą linii u dołu i po prawej. Na górnym obrazie przerywane szare linie wskazują obszary obrazu, które są powielane w celu rozciągnięcia obrazu. Różowy prostokąt na dolnym obrazie wskazuje region, w którym zawartość widoku jest dozwolona. Jeśli zawartość nie mieści się w tym regionie, obraz jest rozciągany, aby się w nim zmieściła.
Narzędzie Draw 9-patch to bardzo wygodny sposób tworzenia obrazów NinePatch za pomocą edytora graficznego WYSIWYG. Wyświetla nawet ostrzeżenia, jeśli zdefiniowany przez Ciebie obszar rozciągliwy może powodować artefakty rysunkowe w wyniku replikacji pikseli.
Ten przykładowy układ XML pokazuje, jak dodać grafikę NinePatch do kilku przycisków. Obraz NinePatch zostanie zapisany 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"/>
Zwróć uwagę, że atrybuty layout_width i layout_height są ustawione na wrap_content, aby przycisk dobrze pasował do tekstu.
Ilustracja 2 przedstawia 2 przyciski wyrenderowane z pliku XML i obrazu NinePatch pokazanych powyżej. Zwróć uwagę, jak szerokość i wysokość przycisku zmieniają się w zależności od tekstu, a obraz tła rozciąga się, aby się do niego dopasować.
Rysunek 2. Przyciski renderowane za pomocą zasobu XML i grafiki NinePatch
Niestandardowe elementy rysowalne
Jeśli chcesz utworzyć rysunki niestandardowe, możesz to zrobić, rozszerzając klasę Drawable (lub dowolną z jej podklas).
Najważniejsza metoda do wdrożenia to draw(Canvas), ponieważ udostępnia ona obiekt Canvas, którego musisz użyć, aby podać instrukcje rysowania.
Poniższy kod pokazuje prostą podklasę 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 rysowalny w dowolnym miejscu, np. do elementu 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));
Na Androidzie 7.0 (poziom interfejsu API 24) i nowszym możesz też definiować instancje niestandardowego obiektu Drawable za pomocą kodu XML w ten sposób:
- Użyj pełnej 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żyj
drawablejako nazwy tagu XML i określ pełną nazwę klasy z atrybutu klasy. Można jej 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" />
Dodawanie odcienia do elementów rysowalnych
W Androidzie 5.0 (poziom interfejsu API 21) i nowszym możesz zmieniać kolor map bitowych i obrazów 9-patch zdefiniowanych jako maski alfa. Możesz je barwić za pomocą zasobów kolorów lub atrybutów motywu, które są rozwiązywane do zasobów kolorów (np. ?android:attr/colorPrimary). Zwykle tworzysz te komponenty tylko raz i automatycznie kolorujesz je tak, aby pasowały do motywu.
Za pomocą metody setTint() możesz zastosować odcień do obiektów BitmapDrawable, NinePatchDrawable lub VectorDrawable. W układach możesz też ustawić kolor i tryb zabarwienia za pomocą atrybutów android:tint i android:tintMode.
Wyodrębnianie dominujących kolorów z obrazu
Biblioteka pomocy Androida zawiera klasę Palette, która umożliwia wyodrębnianie z obrazu dominujących kolorów.
Możesz wczytać zasoby rysowalne jako Bitmap i przekazać je do Palette, aby uzyskać dostęp do ich kolorów.
Więcej informacji znajdziesz w artykule Wybieranie kolorów za pomocą interfejsu Palette API.