Grafika 2D //Paint - jak rysowac? //Canvas - co rysowac? 0. Tworzymy aplikację Android: a. Nazwa: Grafika2D b. Template: Blank App c. Minimum Android Version: Android 5.0 (Lollipop) 1. Plik activity_main.xml niewiele nas obchodzi, bo stworzymy własny widok w kodzie, który nie będzie opierał się na pliku Resource/layout/activity_main.axml. Ten ostatni można nawet usunąć. 2. Jeżeli chcemy wymusić orientację poziomą aplikacji na urządzeniu, musimy zmienić manifest. Ale nie bezpośrednio, a zmieniając atrybut klasy MainActivity: [Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true, ScreenOrientation = Android.Content.PM.ScreenOrientation.Landscape)] public class MainActivity : AppCompatActivity { Możemy też ustawić domyślne Android.Content.PM.ScreenOrientation.Unspecified i obrócić emulator (ikony na bocznym pasku). 3. Do projektu dodajemy plik klasy Grafika2DView.cs i uzupełniamy kod klasy: using Android.Content; using Android.Graphics; using Android.Util; using Android.Views; namespace Grafika2D { public class Grafika2DView : View { private Paint paint; int szerokość, wysokość; public Grafika2DView(Context context, IAttributeSet attributeSet) :base(context, attributeSet) { paint = new Paint(PaintFlags.AntiAlias); paint.SetStyle(Paint.Style.FillAndStroke); paint.StrokeWidth = 3; szerokość = this.Width; wysokość = this.Height; Log.Debug("View.ctor", "Szerokość: " + szerokość.ToString()); Log.Debug("View.ctor", "Wysokość: " + wysokość.ToString()); SetBackgroundColor(Color.Cyan); } } } Uwaga! Rozmiary widoku nie są znane w konstruktorze. Tu odczytana szerokość i wysokość będą równe zeru. Te parametry należy odczytać w metodzie Grafika2DView.OnSizeChanged. 4. Zróbmy to. Przy okazji uwrażliwimy aplikację na zmianę orientacji ekranu: protected override void OnSizeChanged(int w, int h, int oldw, int oldh) { base.OnSizeChanged(w, h, oldw, oldh); szerokość = Width; wysokość = Height; Log.Debug("View.onSizeChanged", "Szerokość: " + szerokość.ToString()); Log.Debug("View.onSizeChanged", "Wysokość: " + wysokość.ToString()); } 7. Instrukcje rysowania należy umieścić w metodzie Grafika2DView.OnDraw: Na początek przećwiczymy metody Paint.DrawCircle, DrawLine: protected override void OnDraw(Canvas canvas) { base.OnDraw(canvas); paint.SetStyle(Paint.Style.FillAndStroke); paint.StrokeWidth = 3; //słońce paint.Color = Color.Yellow; { int x0 = 4 * szerokość / 5; int y0 = wysokość / 4; int r1 = wysokość / 10; int r2 = 2 * r1; canvas.DrawCircle(x0, y0, r1, paint); int ilePromieni = 8; double wsp = 2 * Java.Lang.Math.Pi / ilePromieni; for (int i = 0; i < ilePromieni; ++i) { canvas.DrawLine(x0, y0, x0 + r2 * (float)Java.Lang.Math.Cos(wsp * i), y0 + r2 * (float)Java.Lang.Math.Sin(wsp * i), paint); } } } 8. Aby zmienić widok zmieniamy argument metody SetContentView w metodzie MainActivity.OnCreate: namespace Grafika2D { [Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true, ScreenOrientation = Android.Content.PM.ScreenOrientation.Unspecified)] public class MainActivity : AppCompatActivity { protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); // Set our view from the "main" layout resource //SetContentView(Resource.Layout.activity_main); SetContentView(new Grafika2DView(ApplicationContext, null)); } } } 9. W metodzie Grafika2DView.OnDraw nie powinno być metod inicjujących obiekty. Ze względu na wydajność te należy utworzyć np. w OnSizeChanged. a. Deklarujemy prywatne pola klasy Grafika2DView: private Rect prost1, prost2, prost3; b. Definiujemy je w metodzie onSizeChanged: //obiekty wykorzystywane w OnDraw prost1 = new Rect(0, wysokość / 2, szerokość, wysokość); prost2 = new Rect(szerokość / 10, wysokość / 3, 5 * szerokość / 10, 2 * wysokość / 3); prost3 = new Rect(7 * szerokość / 40, 9 * wysokość / 20, 5 * szerokość / 20, 2 * wysokość / 3); c. Wykorzystujemy je w onDraw dodajac do niej polecenia: //ziemia paint.Color = Color.Argb(255, 191, 255, 0); canvas.DrawRect(prost1, paint); //ściany paint.Color = Color.White; canvas.DrawRect(prost2, paint); //drzwi paint.Color = Color.Argb(255, 0, 125, 0); canvas.DrawRect(prost3, paint); //okno paint.Color = Color.Cyan; for (int ix = 0; ix < 2; ++ix) { for (int iy = 0; iy < 2; ++iy) { int x0 = 13 * szerokość / 40; int y0 = 2 * wysokość / 5; int x1 = x0 + ix * szerokość / 15; int y1 = y0 + iy * wysokość / 15; int x2 = x1 + szerokość / 20; int y2 = y1 + wysokość / 20; canvas.DrawRect(new Rect(x1, y1, x2, y2), paint); } } 10. Usunięcie paska tytułu - zmieniamy temat określony w pliku Resources/values/styles.xml 11. Rysowanie dowolnego kształtu - definiowanie ścieżek (wracamy do edycji klasy Grafika2DView) a. Pole: private Path ścieżka; b. W metodzie OnSizeChanged (polecenia a la Logo): ścieżka = new Path(); ścieżka.SetFillType(Path.FillType.EvenOdd); ścieżka.MoveTo(szerokość / 10, wysokość / 3); ścieżka.LineTo(2 * szerokość / 10, wysokość / 6); ścieżka.LineTo(4 * szerokość / 10, wysokość / 6); ścieżka.LineTo(5 * szerokość / 10, wysokość / 3); ścieżka.LineTo(szerokość / 10, wysokość / 3); ścieżka.Close(); c. Wreszcie w OnDraw: //dach paint.Color = Color.Red; canvas.DrawPath(ścieżka, paint); 12. Rysowanie bitmapy z zasobow: a. Pole: private Bitmap obraz; b. Metoda OnSizeChanged: obraz = BitmapFactory.DecodeResource(Resources, Resource.Mipmap.ic_launcher); if (obraz == null) Log.Error("View.ctor", "Obraz nie został poprawnie wczytany"); Uwaga! Niestety w Xamarin rysunek zielonego robota zastąpiła ikona "X" c. Metoda OnDraw: //obraz { int x0 = szerokość / 2; int y0 = 4 * wysokość / 10; float wsp = (float)obraz.Height / obraz.Width; int dx = szerokość / 5; int dy = (int)(y0 * wsp); canvas.DrawBitmap(obraz, null, new Rect(x0, y0, x0 + dx, y0 + dy), paint); } 13. Animacja rysunku androida (cykliczne przesuwanie w lewo i w prawo): a. Definiujemy pola: private long aktualnyCzas = -1, poprzedniCzas = -1; b. W metodzie OnDraw: //czas do animacji if (aktualnyCzas < 0) { aktualnyCzas = Java.Lang.JavaSystem.CurrentTimeMillis(); poprzedniCzas = aktualnyCzas; } else { poprzedniCzas = aktualnyCzas; aktualnyCzas = Java.Lang.JavaSystem.CurrentTimeMillis(); } const double f = 0.001; //android - animacja { int x0 = szerokość / 2; int y0 = 4 * wysokość / 10; float wsp = (float)obraz.Height / obraz.Width; int dx = szerokość / 5; int dy = (int)(y0 * wsp); int xs = (int)(szerokość / 4 * Java.Lang.Math.Sin(f * aktualnyCzas)); //dodane!!! canvas.DrawBitmap(obraz, null, new Rect(x0 + xs, y0, x0 + dx + xs, y0 + dy), paint); //zmienione!!! } Invalidate(); //animacja, wymuszamy kolejne wywolanie OnDraw - nie do końca eleganckie, lepiej timer } 10. Płot na pierwszym planie Kolory niestandardowe - najlepiej umiescic definicję w zasobach: [Zadanie dla studentów!!] a. Do res/values/colors.xml dodajemy: #2c3e50 #1B3147 #3498db #FFCD7F32 <-------------------- b. W metodzie Grafika2DView.OnDraw (przed wywolaniem metody Invalidate!!): //płot //paint.Color = Color.Argb(255,150,75,0); //paint.Color = Resources.GetColor(Resource.Color.brazowy); //obsolete paint.Color = new Color(Android.Support.V4.Content.ContextCompat.GetColor(Context, Resource.Color.brazowy)); { int x0 = szerokość / 15; int y0 = 6 * wysokość / 10; int dx = szerokość / 25; int y1 = 9 * wysokość / 10; canvas.DrawRect(new Rect(x0, y0 + dx, szerokość - x0 - dx, y0 + 3 * dx / 2), paint); canvas.DrawRect(new Rect(x0, y1 - 3 * dx / 2, szerokość - x0 - dx, y1 - dx), paint); for (int x = x0; x <= szerokość - x0 - dx; x += x0) { canvas.DrawRect(new Rect(x, y0, x + dx, y1), paint); canvas.DrawArc(new RectF(new Rect(x, y0 - dx / 2, x + dx, y0 + dx / 2)), 180, 180, true, paint); //te wszystkie obiekty nie powinny byc tu inicjowane } } Invalidate(); Uwaga! Wszystkie te obiekty, włączając kolor, powinny być inicjowane gdzie indziej. 11. "Rysowanie" napisu z częstością odświeżania. W metodzie onDraw (przed wywolaniem invalidate!!): //napis paint.Color = Color.Black; paint.StrokeWidth = 0.3f; paint.TextSize = 30f; //string tekst = "(c) Jacek Matulewski 2018"; double interwał = aktualnyCzas - poprzedniCzas; interwał /= 1000; //[ms] -> [s] double fi = Java.Lang.Math.Round(100 / interwał) / 100.0; string tekst = "FPS: " + string.Format("{0:00.00}", fi); //f w [Hz]=[1/s]=[FPS] float w = paint.MeasureText(tekst); float margines = wysokość / 30; canvas.DrawText(tekst, szerokość - w - margines, wysokość - margines, paint); Invalidate();