Szkolenia programowania we Wrocławiu
Kurs Android

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
  • miana 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 wigetó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.

Paczka z całym programem dostępna jest tutaj.

 

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.

Komentarze

Komentarze zamknięte. Zapraszamy do grupy na Facebooku
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 :(

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.

Adrian

Mam ten sam problem co kolega powyżej, nie wiem co jest przyczyną.

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 ?