niedziela, 20 marca 2016

[DSP2016] Android prosto z poligonu odc.1 (lista, appbar, nawigacja, ikony, skórki)

Teraz powstanie jakaś seria postów, do których będę przelewał swój aktualny strumień świadomości powstający na skutek obcowania z Android Studio i kodzenia mającego zmierzać do celu, jakim jest Light Organ.

Jesteśmy na początku drogi, “gramy na nowym instrumencie”, ale mamy pierwszą krótką “sesję” za sobą. Co się udało podczas niej “zarejestrować” na dysku i githubie ?

device-2016-03-20-020919  device-2016-03-20-021016

Te dwa ekrany w początkowej, szkicowej formie na razie nie powalają, ale od czegoś trzeba zacząć.

Mała dygresja. Jak zrobiłem screenshoty z emulatora?  Jak się wybierze Android Device Monitor z górnego paska Android Studio, to na dole pojawia się nam panel, gdzie w lewym górnym rogu mamy przycisk do robienia screenshotów.

screen

Przejdźmy teraz do wstępnej listy plików muzycznych, z których będzie można wybrać sobie coś do zagrania. Na obecną chwilę wrzuciłem tam przykładowe dane, wygląd też jest jeszcze surowy. Ale możemy napisać tutaj parę słów o komponencie do prezentowania danych z listy. Nie użyłem tradycyjnego ListView, a czegoś lepszego i nowszego – RecyclerView. Pozwala to obsługiwać duże kolekcje (na telefonie też moze się uzbierać trochę muzyki), które są aktualizowane wskutek różnych działań (ktoś może pobierać sobie np. coś). Co więcej pozwala w prosty sposób zmieniać rodzaj layoutu np. z listy na grid (czy nawet zdefiniować własny) , co ułatwia robienie responsywnego interfejsu użytkownika na różne urządzenia. Komponent pozwala też na podanie szablonu XML do elementu (widzę tu jakieś podobieństwo z XAML –;))  Najprostsza surowa lista, bez podmiany rodzaju layoutu wymagała markupu w activity_file_list.xml:

<android.support.v7.widget.RecyclerView
android:id="@+id/item_list"
android:name="com.apps.kruszyn.myapplication.ItemListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/file_list_item" />

markupu w file_list_item.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/text_margin"
android:layout_marginBottom="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem"/>

<TextView
android:id="@+id/artist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/text_margin"
android:layout_marginTop="@dimen/text_margin"
android:layout_marginBottom="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem"/>

<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/text_margin"
android:layout_marginTop="@dimen/text_margin"
android:layout_marginBottom="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem"/>

</LinearLayout>

oraz kodu w FileListActivity:


public class FileListActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private RecyclerView.Adapter mAdapter;
    private RecyclerView.LayoutManager mLayoutManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_list);

        mRecyclerView = (RecyclerView) findViewById(R.id.item_list);

        mRecyclerView.setHasFixedSize(true);

        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);

        mAdapter = new SimpleItemRecyclerViewAdapter(SampleData.MEDIA_FILE_ITEMS);
        mRecyclerView.setAdapter(mAdapter);
    }

    public class SimpleItemRecyclerViewAdapter
            extends RecyclerView.Adapter<SimpleItemRecyclerViewAdapter.ViewHolder> {

        private final List<MediaFileItem> mValues;

        public SimpleItemRecyclerViewAdapter(List<MediaFileItem> items) {
            mValues = items;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.file_list_item, parent, false);
            return new ViewHolder(view);
        }

        @Override
        public void onBindViewHolder(final ViewHolder holder, int position) {
            holder.mItem = mValues.get(position);
            holder.mTitleView.setText(mValues.get(position).title);
            holder.mArtistView.setText(mValues.get(position).artist);
            holder.mDurationView.setText(DateUtils.formatElapsedTime(mValues.get(position).duration / 1000));

            …
            
        }

        @Override
        public int getItemCount() {
            return mValues.size();
        }

        public class ViewHolder extends RecyclerView.ViewHolder {
            public final View mView;
            public final TextView mTitleView;
            public final TextView mArtistView;
            public final TextView mDurationView;
            public MediaFileItem mItem;

            public ViewHolder(View view) {
                super(view);
                mView = view;
                mTitleView = (TextView) view.findViewById(R.id.title);
                mArtistView = (TextView) view.findViewById(R.id.artist);
                mDurationView = (TextView) view.findViewById(R.id.duration);
            }
        }
    }
}

Trochę tego jest, polecam też rzucenie okiem do dokumentacji http://developer.android.com/training/material/lists-cards.html.
By oddać sprawiedliwość, w iOS obsługa list nie wygląda lepiej. Z kolei pamiętam, że w Windows 8.1 w celu wydajnego renderowania list też skomplikowano nieco bardziej sposób implementacji, wprowadzono imperatywne operowanie na danych (tamte fazy to jednak coś innego).
Aby przejść do ekranu z listą potrzebujemy na ekranie startowym dodać do appBar akcję z ikoną nuty. Kreator Android Studio utworzył mi w czasie poprzedniego postu na startowej aktywności taki markup:
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>
Mimo jednak coraz nowszych komponentów paska akcji tak naprawdę nadal definiujemy akcje na starym menu Androidowym w res/menu/menu_main.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.apps.kruszyn.lightorganapp.MainActivity">

<item
android:id="@+id/action_media_files"
android:icon="@drawable/ic_library_music_white_48dp"
android:title="@string/action_media_files"
app:showAsAction="always"/>

<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>
czy w oldschoolowy nadal sposób obsługujemy do niego callbacki w MainActivity (ekran startowy):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.

switch (item.getItemId()) {
case R.id.action_media_files:

Intent intent = new Intent(this, FileListActivity.class);
startActivity(intent);

return true;

case R.id.action_settings:
return true;
}

return super.onOptionsItemSelected(item);
}

Przy okazji widzimy tutaj, jak możemy przenawigować z jednego ekranu do innego. Tworzymy Intent z klasą docelowej aktywności, możemy ustawić na nim parametry do przekazania (tutaj nie skorzystałem z tego) i na końcu wywołać startActivity.

Nasuwa się tutaj mała dygresja związana z definiowaniem aktywności w projekcie. W AndroidManifest.xml odnajdujemy ich deklaracje. Jeśli będziemy mieli prostą deklarację w stylu:

<activity
android:name=".FileListActivity"
android:label="@string/file_list_activity_name"/>

to otrzymamy wygląd taki jak u mnie na początku tego postu. Zwróćmy przy okazji uwagę na android:label, którym określamy tytuł wyświetlany na appbar aktywności (używam zdefiniowanego przeze mnie zasobu string). Gdyby zastosować definicję taką jak poniżej:

<activity
android:name=".FileListActivity"
android:label="@string/app_name"
android:parentActivityName=".MainActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>

to w appbar muzycznej biblioteki pojawiłaby się strzałka wstecz, dodatkowo ta strona otworzyłaby się z nieco inną animacją. Skąd tak przekombinowałem? Robiłem zgodnie z http://developer.android.com/training/basics/firstapp/starting-activity.html

Wróćmy teraz do samej ikony akcji. Po pracy w Blendzie łudziłem się, że może ikona mi się sama wstawi po podaniu nazwy zasobu… Nic z tych rzeczy!  Ikony Material Design (oczywiscie całą app-kę robię zgodnie wstecz z Material Design) możemy pobierać sobie ze strony https://design.google.com/icons/.  Następnie ikonę muzycznej biblioteki załączyłem ręcznie do folderów drawable-* w projekcie (wybrałem wariant biały)

Na koniec coś o kolorystyce i skórkach. W poprzednim poście miałem jasną skórkę i niebieskie paski. Od teraz jednak przeszedłem na ciemną tonację. To ma być aplikacja muzyczna, w dodatku ze światłami, dlatego taka decyzja. Zmieniłem dwie definicje skórek (dwie inne pozostały jak wcześniej) w pliku res/values/styles.xml z:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />

na:

<style name="AppTheme" parent="Theme.AppCompat"/>
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Dark" />

 

Co dokładnie oznaczają te definicje? Najlepiej przeczytać dokumentację ze stron:

To tyle na dzisiaj. W najbliszym czasie pasuje lepiej ostylować tę listę i dorobić do niej searcha, no i oczywiście… podpiąć prawdziwe pliki.

Na zakończenie dodam, że warto patrzeć oczywiscie też na sample z SDK (można je sobie ładować z poziomu strony startowej Android Studio).

Brak komentarzy: