poniedziałek, 11 kwietnia 2016

[DSP2016] Android prosto z poligonu odc.7 (grafika 2D, własny widok)

W naszej deweloperskiej wędrówce nadeszła pora na światła (narysowanie jeszcze bez sterowania). Obecna wersja na githubie prezentuje się w tym zakresie następująco:

Screenshot_20160411-070900  Screenshot_20160411-070933 

Screenshot_20160411-070915 

Screenshot_20160411-070942 

Jak technicznie to osiągnąłem?

Myśląc o grafice 2D zapoznałem się z linkami:

W pierwszej chwili myślałem, by pójść na całość z operowaniem bezpośrednio na Canvas z rysowaniem na SurfaceView w osobnym wątku. Potem jednak przyszła refleksja pod wpływem patrzenia na kod z przykładu. Jakbym to robił w XAML? Czy bym zaprzęgał do tego aż DirectX? Wraz z odtwarzaniem muzyki kształty nie będą ciągle przerysowywane na zupełnie inne, będzie zmieniana tylko przezroczystość. Nie musimy mieć hiper ultra wydajności, oryginalne kolorofony nawet miały pewne opóźnienia z powodu stososowania żarówek. Nie prościej zrobić to z obiektami ShapeDrawable (w XML lub w kodzie)? Oczywiście jakby rysowanie polegało na ciągłym rysowaniu zupełnie innych figur czy w przyszłości okazało się, że aplikacja ma problemy z wydajnością, rysowanie wpływa na odtwarzanie dźwięku to skorzystamy z bardziej wyrafinowanych możliwości, ale nie sądzę.

Decydując się na grafikę w XML zdefiniowałem sobie plik w res/drawable z root-em shape rodzaju oval. Potem wrzuciłem do LinearLayout trzy View z background-em na ten shape, tak by każdy element zajmował jedna trzecia wysokości (zacząłem od orientacji pionowej), a szerokość była zgodna z parentem. Hurra, mam pierwszy rysunek… ale w sumie lipa. Mam owalne światła i nie wiem jak zmusić oval by były okrągłe. Koła powinny z jednej strony wypełniać dynamicznie określaną przez layout przestrzeń, z drugiej strony ich promienie powinny być wartością minimalną z kierunków x i y. Co tu robić?

Szukałem jakiś czas rozwiązania i okazało się, że trzeba napisać trochę do tego kodu, że XML raczej sam nie wystarczy (myślałem jeszcze nad shape typu ring), co zaprowadziło mnie do pisania własnych widoków. O tym ostatnim oficjalna dokumentacja pisze na stronie http://developer.android.com/training/custom-views/index.html, ale w dalszej implementacji pomógł mi bardziej sam Android Studio. Z jego pomocą dodałem Custom View, a tam mamy przykładowy kod komponentu i jego wykorzystanie z poziomu XML, włącznie z obsługą własnych atrybutów. Po przeróbkach otrzymałem CircleView (ui/CircleView.java):

public class CircleView extends View {

private int mCircleColor = Color.RED;

private Paint mPaint;

public CircleView(Context context) {
super(context);
init(null, 0);
}

public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}

public CircleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}

private void init(AttributeSet attrs, int defStyle) {
// Load attributes
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.CircleView, defStyle, 0);

mCircleColor = a.getColor(
R.styleable.CircleView_circleColor,
mCircleColor);

a.recycle();

mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mCircleColor);
}

private void invalidatePaint() {
mPaint.setColor(mCircleColor);
invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();

int contentWidth = getWidth() - paddingLeft - paddingRight;
int contentHeight = getHeight() - paddingTop - paddingBottom;
float radius = Math.min(contentWidth, contentHeight) / 2;

canvas.drawCircle(paddingLeft + contentWidth / 2,
paddingTop + contentHeight / 2,
radius,
mPaint);
}


public int getCircleColor() {
return mCircleColor;
}

public void setCircleColor(int circleColor) {
mCircleColor = circleColor;
invalidatePaint();
}
}

W pliku res/values/attrs_circle_view.xml mamy zdefiniowany własny atrybut circleColor:

<resources>
<declare-styleable name="CircleView">
<attr name="circleColor" format="color" />
</declare-styleable>
</resources>

Potrzebujemy teraz w skalowalny sposób osadzić światła. Zacznijmy od orientacji pionowej. W tym celu w layout/content_main.xml wewnątrz RelativeLayout wprowadziłem XML:

<include layout="@layout/content_lights"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_above="@id/controls_container"/>

i zdefiniowałem plik layout/content_lights.xml ze światłami:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">

<com.apps.kruszyn.lightorganapp.ui.CircleView
android:id="@+id/bass_light"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:circleColor="#d50000"/>

<com.apps.kruszyn.lightorganapp.ui.CircleView
android:id="@+id/mid_light"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="20dp"
app:circleColor="#ffab00"/>

<com.apps.kruszyn.lightorganapp.ui.CircleView
android:id="@+id/treble_light"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="20dp"
app:circleColor="#6200ea"/>

</LinearLayout>

Czemu tak z osobnym plikiem? Dlatego, że dla orientacji poziomej ekranu definiuję jego alternatywną wersję w folderze layout-land:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">

<com.apps.kruszyn.lightorganapp.ui.CircleView
android:id="@+id/bass_light"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:circleColor="#d50000"/>

<com.apps.kruszyn.lightorganapp.ui.CircleView
android:id="@+id/mid_light"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginLeft="20dp"
app:circleColor="#ffab00"/>

<com.apps.kruszyn.lightorganapp.ui.CircleView
android:id="@+id/treble_light"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginLeft="20dp"
app:circleColor="#6200ea"/>

</LinearLayout>

Teraz możemy się cieszyć responsywnym layoutem, który wyznacza przestrzeń do rysowania świateł w optymalny sposób (w zależności od dostępnej przestrzeni np. obecność dolnego paska sterowania odtwarzaniem i orientacji ekranu).

Z myślą o sterowaniu światłami w przyszłości już teraz sprawdziłem, czy są na to gotowe. Potrzebujemy ustawiać w runtime przezroczystość na podstawie podanego współczynnika. Posiłkując się kodem w jednym z powyższych linków na stackoverflow w MainActivity stworzyłem kod:

private void setLight(@IdRes int id, float ratio)
{
CircleView light = (CircleView) findViewById(id);
light.setCircleColor(getColorWithAlpha(light.getCircleColor(), ratio));
}

private static int getColorWithAlpha(int color, float ratio) {
int newColor = 0;
int alpha = Math.round(Color.alpha(color) * ratio);
int r = Color.red(color);
int g = Color.green(color);
int b = Color.blue(color);
newColor = Color.argb(alpha, r, g, b);
return newColor;
}

Tymczowo na potrzebę tego odcinka wrzuciłem testowy kod ustawiający jasność świateł na 50% po wyborze opcji Settings z przycisku … w górnym toolbarze:

//test
setLight(R.id.bass_light, 0.5f);
setLight(R.id.mid_light, 0.5f);
setLight(R.id.treble_light, 0.5f);

Podsumowując światła już mamy, teraz musimy pomyśleć o sterowaniu nimi przez dźwięk. Do zobaczenia w następnym odcinku.

Brak komentarzy: