Piszemy painta na Androida
Znamy juz większość podstawowych funkcji. Pora na przykładowy program.
Tym razem pora na painta. Wypisze na początek podstawowe funkcjonalności które chcemy osiągnąć:
- dostępne narzędzie: pędzel
- wybór koloru z 3 dostępnych: czerwony, niebieski i zielony
- zmiana wielkości pędzla za pomocą suwaka
- opcje rysowania zmieniane w menu
- aplikacja działająca w trybie pełnoekranowym
Docelowa aplikacja:
Zacznijmy od stworzenia nowego projektu. Następnie przejdźmy do ustawienia layoutu. Tym razem nie będziemy jednak przeciągać gotowych widgetów, stworzymy własny.
Napiszmy następującą klasę:
package pl.javastart;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class PaintView extends SurfaceView implements SurfaceHolder.Callback {
public PaintView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
public PaintView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public PaintView(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
// TODO Auto-generated method stub
}
public void surfaceCreated(SurfaceHolder arg0) {
// TODO Auto-generated method stub
}
public void surfaceDestroyed(SurfaceHolder arg0) {
// TODO Auto-generated method stub
}
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
protected void onDraw(Canvas canvas) {
}
}
Krótkie omówienie:
Jako że potrzebujemy miejsca na którym moglibyśmy w przyszłości rysować, oraz wykrywającego nasz dotyk, oto ono.
Nasza klasa rozszerza SurfaceView dzięki czemu możemy z łatwością obsługiwać rysowanie na całym ekranie. Dodatkowo implementujemy interfejs SurfaceHolder.Callback, co pozwala na przechwytywanie informacji o zmianach ekranu.
Poza tym przesłoniłem dwie metody: onTouchEvent(..) oraz onDraw(...), których będziemy używali do wykrywania dotyku i odpowiedniej obsługi.
Przejdźmy teraz do ustawienia pliku xml z interfejsem. Po wybraniu zakładki Custom i naciśnięciu Refresh powinien pojawić się nasz View.
Layout w jakim go umieścimy jest dowolny. Ja wybrałem LinearLayout. Następnie rozciągnąłem go na cały ekran. Ustawiłem także tło na białe.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<pl.javastart.PaintView
android:id="@+id/paintView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff" />
</LinearLayout>
Jak widać, element został dodany wraz z informacją o paczce w której się znajduje. Powinno działać :)
Zajmijmy się na początku najciekawszym - obsługą dotyku.
public boolean onTouchEvent(MotionEvent event) {
Log.d("dotyk", event.getX()+" "+event.getY());
return true;
//return super.onTouchEvent(event);
}
Ważnym elementem tutaj jest zwracanie false. Jest to parametr stwierdzający, czy dotyk został już "zużyty". Jeśli zwrócimy true, to aktualne dotknięcie nie będzie już sprawdzane, przez co nie uzyskamy informacji o przesunięciu palca.
Wykorzystamy Log, żeby sprawdzić czy wszystko działa jak należy. Więcej informacji o użyciu Logu wkrótce.
Po uruchomieniu aplikacji i przejechaniu palcem powinniśmy ujrzeć odpowiednie informacje w logu:
Znak, że działa.
Nic się jeszcze nie rysuje, zajmijmy się tym teraz. Dodamy jeden stały kolor, później zajmiemy się opcjami.
Plan jest następujący:
Przy każdym dotknięciu ekranu dodajemy element do listy, oraz przerysowujemy cały ekran.
public boolean onTouchEvent(MotionEvent event) {
RectF oval = new RectF(event.getX()-50, event.getY()-50, event.getX() + 50, event.getY() + 50);
punkty.add(oval);
invalidate();
return true;
}
Tworzymy nowy obiekt w którym podajemy koordynaty naszego przyszłego punktu na ekranie, w tym przypadku od początku (lewa strona i góra) odejmuję 50px a do końca (prawa strona i dół) dodaję 50px, żeby środek punku znajdował się w miejscu dotknięcia.
Następnie metoda invalidate() przekazuje informację, że wymagane jest przemalowanie ekranu, czyli w naszym wypadku wywołanie metody odDraw(Canvas canvas)
W metodzie onDraw((...) za każdym razem rysujemy nowe elementy:
protected void onDraw(Canvas canvas) {
paint.setColor(Color.RED);
for (RectF punkt : punkty) {
canvas.drawOval(punkt, paint);
}
}
Aplikacja powinna już działać (screen z mojego telefonu)
Cały kod programu:
package pl.javastart;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class PaintView extends SurfaceView implements SurfaceHolder.Callback {
ArrayList<RectF> punkty;
Paint paint = new Paint();
public PaintView(Context context, AttributeSet attrs) {
super(context, attrs);
punkty = new ArrayList<RectF>();
paint = new Paint();
}
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
// TODO Auto-generated method stub
}
public void surfaceCreated(SurfaceHolder arg0) {
}
public void surfaceDestroyed(SurfaceHolder arg0) {
// TODO Auto-generated method stub
}
public boolean onTouchEvent(MotionEvent event) {
RectF oval = new RectF(event.getX()-50, event.getY()-50, event.getX() + 50, event.getY() + 50);
punkty.add(oval);
invalidate();
return true;
}
protected void onDraw(Canvas canvas) {
paint.setColor(Color.RED);
for (RectF punkt : punkty) {
canvas.drawOval(punkt, paint);
}
}
}
Pozostało dodać menu i odpowiednie opcje.
paint.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/menu_kolor"
android:orderInCategory="1"
android:title="@string/kolor"/>
<item
android:id="@+id/menu_rozmiar"
android:orderInCategory="2"
android:title="@string/rozmiar"/>
</menu>
Zacznijmy od zmiany koloru pędzla.
PaintActivity.java:
package pl.javastart;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.RadioGroup;
import android.widget.Toast;
public class PaintActivity extends Activity {
PaintView paintView;
Context ctx;
RadioGroup radioGroup;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.paint);
paintView = (PaintView) findViewById(R.id.paintView);
ctx = getApplicationContext();
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.paint, menu);
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_kolor:
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
alertDialogBuilder.setTitle("Wybor koloru");
final CharSequence[] items = { "Czerwony", "Zielony", "Niebieski" };
alertDialogBuilder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
switch (item) {
case 0:
paintView.setColor(Color.RED);
break;
case 1:
paintView.setColor(Color.GREEN);
break;
case 2:
paintView.setColor(Color.BLUE);
break;
}
}
});
AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.show();
break;
}
return true;
}
}
To jednak sprawi, że wszystko co narysowaliśmy do tej pory również zmieni kolor. Stwórzmy więc naszą klasę przechowującą obiekty do naysowania z dodatkową informacją o kolorze:
ObiektDoNarysowania.java
package pl.javastart;
import android.graphics.RectF;
public class ObiektDoNarysowania {
public int kolor;
public RectF figura;
public ObiektDoNarysowania(int kolor, RectF figura) {
this.kolor = kolor;
this.figura = figura;
}
}
Musimy teraz odpowiednio zmodyfikować klasę PaintView:
package pl.javastart;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class PaintView extends SurfaceView implements SurfaceHolder.Callback {
ArrayList<ObiektDoNarysowania> punkty;
Paint paint = new Paint();
private int color;
public PaintView(Context context, AttributeSet attrs) {
super(context, attrs);
punkty = new ArrayList<ObiektDoNarysowania>();
paint = new Paint();
color = Color.RED;
}
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
// TODO Auto-generated method stub
}
public void surfaceCreated(SurfaceHolder arg0) {
}
public void surfaceDestroyed(SurfaceHolder arg0) {
// TODO Auto-generated method stub
}
public boolean onTouchEvent(MotionEvent event) {
RectF oval = new RectF(event.getX() - 50, event.getY() - 50, event.getX() + 50, event.getY() + 50);
punkty.add(new ObiektDoNarysowania(color, oval));
invalidate();
return true;
}
protected void onDraw(Canvas canvas) {
for (ObiektDoNarysowania punkt : punkty) {
paint.setColor(punkt.kolor);
canvas.drawOval(punkt.figura, paint);
}
}
public void setColor(int color) {
this.color = color;
}
}
Działa jak marzenie:
Pora na rozmiar pędzla.
Tutaj potrzebny będzie nowy layout. Jako suwaka użyjemy ProgressBar.
rozmiar.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
Dodajmy opcję do menu:
case R.id.menu_rozmiar:
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.rozmiar, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setView(layout);
alertDialog = builder.create();
alertDialog.show();
SeekBar sb = (SeekBar)layout.findViewById(R.id.seekBar);
sb.setMax(30);
sb.setProgress(paintView.getRozmiar()-20);
sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
public void onStartTrackingTouch(SeekBar arg0) {
// TODO Auto-generated method stub
}
public void onStopTrackingTouch(SeekBar arg0) {
// TODO Auto-generated method stub
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
paintView.setRozmiar(20+progress);
}
});
Tutaj znowu tworzę AlertDialog, tym razem dodaję do niego własny widok. Ustawiam maksymalną wartość dla SeekBara. Niestety nie da się ustawić wartości minimalnej, dlatego przy ustawianiu będę dodawał 20, co da nam rozmiar pędzla w przedziale od 20px do 50px.
Podpiąłem listener i przy każdej zmianie ustawiana jest odpowiednia wartość wielkości pędzla.
Jak już zapewne zauważyliście do PaintView dodałem metody getRozmiar() oraz setRozmiar(). Zmodyfikowałem również tworzenie obiektu:
RectF oval = new RectF(event.getX() - rozmiar, event.getY() - rozmiar, event.getX() + rozmiar, event.getY() + rozmiar);
Na koniec dodajmy fulscreena. Wy tym celu dodajemy
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
do Manifestu. Powinno to wyglądać mniej więcej tak:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="pl.javastart"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="15" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".PaintActivity"
android:label="@string/title_activity_paint"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
No i na tym zakończyliśmy tworzenie painta.
Zadania do własnego wykonania:
Poziom początkujący
-
dodaj kolor biały, czarny i fioletowy
-
dodaj opcję czyszczenia ekranu
Poziom średnio zaawansowany:
-
dodaj informację o aktualnym rozmiarze pędzla pod suwakiem
-
dodaj kwadratowy pędzel i możliwość wyboru typu pędzla
Dzięki za uwagę. Problemy, pytania? Komentarze są do Waszej dyspozycji.
Dyskusja i komentarze
Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.
Poniżej znajdziesz archiwalne wpisy z czasów, gdy strona była jeszcze hobbystycznym blogiem.
Dariusz
Ja stworzyłem już:) Jeszcze dodałem wiele funkcji:)
Piotrek
To jak możesz to podziel się swoja aplikacją :)
Krzysiek_Cr
Ciekawy artykuł
marcin1509
Bardzo fajny artykuł :)
Tomek
Witam, mam problem z funkcją czyszczącą ekran, mianowicie dodałem sobie metode clear() w klasie PaintView. W tej metodzie usuwam wszystkie elementy z listy punkty i chciałbym sztucznie wywołać metodę onDraw. Jak to zrobić?
Marcin Kunert
Bardzo dobre podejście do tego zadania. Tutaj cytat z artykułu, odpowiadający na Twoje pytanie: „Następnie metoda invalidate() przekazuje informację, że wymagane jest przemalowanie ekranu, czyli w naszym wypadku wywołanie metody odDraw(Canvas canvas)”
Tomek
Dzięki :)
Piotrek
Cześć Czy jeśli chcę dodać dodatkowy pędzel do programu, to w jaki sposób mam to zrobić ? dodatkowa klasa? w tej formie programu domyślnie jest że maluje kółka. P.S. Kiedy kolejne ćwiczenia ? :)
Marcin Kunert
Potrzebna będzie zmiana w: @Override protected void onDraw(Canvas canvas) { for (ObiektDoNarysowania punkt : punkty) { paint.setColor(punkt.kolor); canvas.drawOval(punkt.figura, paint); } } Aktualnie program zawsze rysuje kółka. Warto tutaj dodać warunek, co ma być rysowane, natomiast przy zapisywaniu do tablicy zwrócić uwagę na to jaki pędzel jest aktualnie używany. Potrzebna będzie oczywiście dodatkowa opcja w menu.
troni
hej mam pytanie, robię wszystko jak wyżej i już na samym początku( w miejscu gdzie jest screen z Twojego ekranu) aplikacja uruchamia się lecz po chwili pojawia się : 'unfortunately aplication has stopped'...czy wiecie co może być przyczyną?
alon
Zrób więcej takich przykładowych programów. To naprawdę super rzecz!
Piotrek
Cisza oznacza, że nie mam na co liczyć na odpowiedź ?
Grzesiek
Mam problem z odpaleniem aplikacji, przy pierwszym kodzie podkreśla mi metody surfaceChanged, surfaceCreated i surfaceDestroyed, pisze że nie mogą być przesłonięte, a jak usuwam wpis @Override to na telefonie aplikacja wywala się po kilku sekundach. Co może być nie tak? Apka helloworld na telefonie działa ok.
Marcin Kunert
Na pewno dodałeś informację o rozszerzaniu SurfaceView? (extends SurfaceView) Jeśli tak, to upewnij, się, że została ona zaimportowana (Ctrl+Shift+O załatwi to za Ciebie)
Grzesiek
Tak, wszystko teraz przekopiowałem z poradnika wyżej do src i klasy PaintView. zaimportowane mam wszystko i dalej podkreśla :(
Adrian
Mam ten sam problem co kolega powyżej, nie wiem co jest przyczyną.
Marcin Kunert
Spakuj projekt i prześlij mi na marcin@javastart.pl. Obczaje.
Marcin
Sprawdziłem i okazało się, że adnotacje były niepotrzebne. Metody surfaceChanged, surfaceCreated oraz surfaceDestroyed implementujemy ze względu na interfejs SurfaceHolder.Callback, a nie rozszerzenie klasy SurfaceView. @Override stosuje się tylko przy rozszerzaniu klasy, a nie przy implementacji interfejsu. Sorry za wprowadzenie w błąd. Artykuł oczywiście poprawiłem.
Alex
Świetna robota! Mam taką wskazówkę żeby podawać dłuższą ścieżkę bo czasami można się pogubić po plikach w szczególności gdy mają taką samą nazwę (np. paint.xml). Czekam na następne artykuły!
Damian
Witam. Co należałoby zmienić w aplikacji aby przy szybkim ruchu palcem na ekranie te kropki (czy inne kształty) rysowały się bez przerw? obecnie przy niewielkim rozmiarze pędzla i w miarę szybkim rysowaniu kształty rysują się co ileś tam punktów przez co linia jest poprzerywana. Pozdrawiam.
mateusz
Witam, mam problem gdzie wpisać tą część w jakim pliku/folderze ? public boolean onTouchEvent(MotionEvent event) { Log.d("dotyk", event.getX()+" "+event.getY()); return true; //return super.onTouchEvent(event); } oraz tą; public boolean onTouchEvent(MotionEvent event) { Log.d("dotyk", event.getX()+" "+event.getY()); return true; //return super.onTouchEvent(event); } proszę o wyrozumiałość bo to moje początki w javie.
Piotrek
Mam pytanie dlaczego u mnie na Virtualne maszynie nie ma na dole do wyboru kolor i rozmiar ? Czy to normalne ?
Wojtek
Witam. Przyłącze się do kilku poprzednich osób z pytaniem kiedy planujesz kolejny tutorial? Jeśli ktoś z was zna strony gdzie tutek na jakiś prosty program jest omówiony po polsku to proszę niech się podzieli :)
Przemek
Witam. Możemy liczyć na więcej tego typu tutoriali tym razem jakiś większy projekt?
pawel
pozycje z menu: menu_kolor i menu_rozmiar nie dodały się automatycznie do R.java, więc MainActivity wywala błąd gdy się do nich próbuje odwołać. Męczę się z tym już 3 godziny i nie moge sobie poradzić - proszę o pomoc.
pawel
Już wiem dlaczego nie działało! Wszystko fajnie, ale człowieku jeśli piszesz tutorial i w kodzie na stronie nie umieszczasz pod niektórymi metodami "@Override", a po ściągnięciu i przeanalizowaniu kodu okazuje się, że jest to "@Override" to nie ma sie co dziwić... Dziwne, że inni nie mieli tego problemu... Ogólnie artykuły super, pomocne, ale ta mała kwestia bardzo irytująca.
Marcin Kunert
Hej Paweł! Dzięki za informacje. Niedawno usuwałem te @Override z artykułu, ale zapomniałem o paczce. Własnie ją ściągnąłem i mi się wszystko kompiluje. Próbuję dojść, dlaczego Ci nie działa. Mógłbyś podać jakieś informacje na temat Twojego środowiska? Jakiego masz eclipsa/jave/wersje androida?
madzialenka
dolaczam sie do zapytania uzytkownika #20 Damian,co nalezy zrobic aby przy szybszym przesuwaniu mniejszego pedzla nie bylo przerw miedzy kolejnymi punktami?
Sławek Ludwiczak
Zamiast rysować pojedyncze kropki rysuj krawędzie pomiędzy kolejnymi punktami. Przy bardzo szybkich ruchach nadal może być drobny problem z lekko kanciastą geometrią. Istnieją dalsze, nieco bardziej skomplikowane rozwiązania tego problemu, z pewnością znajdziesz je np na stackoverflow :)
madzialenka
dziekuje bardzo za (p)odpowiedz; )
Stefan
CO zrobić żeby przed odpaleniem aplikacji wyświetlało się jej logo np. jakiś obrazek a dopiero po ok 3-4 sec. widoczne było główne okno aplikacji ?
Marcin Kunert
Hej. Ogólnie to powinno się unikać takich praktyk, ponieważ zmuszają użytkownika do niepotrzebnego czekania. Wybaczalne jesst w momencie, gdy ładujemy dane aplikacji w tle. Postaram się zapoznać z tym tematem i go opisać.
Marcin Kunert
Zgodnie z obietnicą: http://javastart.pl/przykladowe-programy/ekran-ladowania-splash-screen/
madzialenka
chcialabym jeszcze uzyskac wskazowke na temat zapisywania tego co stworzylismy, w jakims jednym wybranym folderze,w dostepnym w srodowisku eclipse, na przyklad ktoryms z drawable. czy obraz trzeba zapisac jako bitmape,tak jak w roznych algorytmach?moze jest jakas cudowna funckja do tego,czy raczej trzeba sie wysilic?
lolek
Załadowałem paczkę z linku na końcu artykułu. Mam tylko biały ekran po którym można pisać na czerwono. Nie ma opcji zmiany kolorów, ani też żadnego menu?
Rafal
Sprobuj odpalic aplikacje na urzadzeniu, nie na emulatorze. Na urzadzeniu powinno pojawic sie Menu z opcjami zmiany koloru i rozmiaru pedzla
lolek
Nie odpalałem na emulatorze. Zawsze na urządzeniu.
Rafal
paintView.setBackgroundColor(Color.parseColor("#00ff00")); rozwiazalo moj problem dynamicznej zmainy tla
Rafal
Czy moge skorzystac z metody remove, aby usuwac tylko elementy dodtkniete (gumka)? np.: punkty.remove(new ObiektDoNarysowania(color, oval)); Niestety nie udalo mi sie uzyskac efektu usuwania tego elementu z listy a metoda punkty.clear(); czysci od razy caly ekran a nie tylko dotkniety punkt. Pomocy :)
Party
Mam pytanie, jak zrobić żeby to co narysuje zapisać potem na kartę pamięci sd ?
TwojMistrz
znow.. przepraszam : Fatalny błąd!! Siedziałem z godzinkę nad tym, ale widać, że kto szuka ten znajduje: Błąd występuje w kodzie xml <item> android:id="@+id/menu_kolor" android:orderInCategory="1" android:title="@string/kolor"/< <item/> Czyli brakuje po prostu zamykającego nawiasu z Być może w starszych wersjach anulowało, lecz teraz działa świetnie