Лабораторная работа 9
Тема: Разработка мобильного приложения, часть 2.
Задание:
Ход работы:
Создаем SplashActivty, который делаем стартовым activity нашего приложения.
Экспортируем из AdobeXD созданный splash screen (выделяем artboard и экспортируем его в формате SVG).
Добавляем svg в drawables, после чего устанавливаем его как background макета Splash Activity.
Устанавливаем стиль в themes.xml, который позволяет убрать actionbar, устанавливаем statusbar в цвет primary, а также настраиваем цветовую палитру
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Give" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/primaryColor</item>
<item name="colorPrimaryVariant">@color/primaryDarkColor</item>
<item name="colorOnPrimary">@color/primaryTextColor</item>
<item name="colorSecondary">@color/secondaryColor</item>
<item name="colorSecondaryVariant">@color/secondaryDarkColor</item>
<item name="colorOnSecondary">@color/secondaryTextColor</item>
<item name="android:colorBackground">@color/white</item>
<item name="colorSurface">@color/white</item>
<item name="colorError">@color/red_600</item>
<item name="colorOnBackground">@color/black</item>
<item name="colorOnSurface">@color/black</item>
<item name="colorOnError">@color/white</item>
<item name="android:statusBarColor">@color/primaryColor</item>
</style>
</resources>
Добавляем код, который позволяет создать полноэкранный activity, а также скрываем actionbar
Делаем заглушку для перехода из SplashScreen в MainActivity. В нашем случае произойдет безусловный переход через секунду
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_splash);
new Handler().postDelayed(() -> {
Intent i = new Intent(this, MainActivity.class);
startActivity(i);
}, 1000);
}
}
Также устанавливаем navigationbar одного цвета с фоном
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setNavigationBarColor(getColor(R.color.primaryColor));
setContentView(R.layout.activity_splash);
new Handler().postDelayed(() -> {
Intent i = new Intent(this, MainActivity.class);
startActivity(i);
}, 1000);
}
}
Далее переходим в MainAcitivity. Нам необходимо создать нижнее меню как в макете
Такой виджет называется BottomAppBar и он работает в паре с CoordinatorLayout. Добавляем в макет CoordinatorLayout, добавляем BottomAppBar, добавляем FAB и устанавливаем привязку к BottomAppBar.
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".main.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
app:backgroundTint="@color/primaryColor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="@id/bottomAppBar"
android:src="@drawable/fab_logo"
app:maxImageSize="40dp"
app:tint="@color/white"
app:rippleColor="@color/secondaryDarkColor"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Добавляем в drawables вектор логотипа и устанавливаем его как кнопку для FAB. В MainActivity.java устанавливаем navigatiobar одного цвета с primary color.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setNavigationBarColor(getColor(R.color.primaryColor));
setContentView(R.layout.activity_main);
}
}
С помощью атрибутов app:fab... устанавливаем нужное положение fab внутри bottomAppBar.
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
app:backgroundTint="@color/primaryColor"
android:layout_width="match_parent"
app:fabCradleVerticalOffset="0dp"
app:fabCradleRoundedCornerRadius="16dp"
app:fabCradleMargin="8dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
Добавляем иконки и меню для BottomAppBar. Если устанавливать иконки в виде обычного меню, они будут располагаться справа, как пункты меню в toolbar. На наше счастье BottomAppBar поддерживает возможность установить собственный макет, чем мы и воспользуемся.
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:backgroundTint="@color/primaryColor"
app:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
android:gravity="fill"
android:paddingBottom="0dp"
app:fabCradleRoundedCornerRadius="16dp"
app:fabCradleVerticalOffset="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageButton
android:id="@+id/map"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless" android:gravity="center"
android:src="@drawable/ic_map" />
<ImageButton
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless" android:gravity="center"
android:src="@drawable/ic_list" />
<ImageButton
android:id="@+id/dummy_image"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:visibility="invisible"
android:background="?attr/selectableItemBackgroundBorderless" android:gravity="center"
android:src="@drawable/ic_map" />
<ImageButton
android:id="@+id/notifications"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless" android:gravity="center"
android:src="@drawable/ic_notification" />
<ImageButton
android:id="@+id/account"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless" android:gravity="center"
android:src="@drawable/ic_account" />
</LinearLayout>
</com.google.android.material.bottomappbar.BottomAppBar>
При запуске приложения на некоторых телефонах обнаружено, что изображение splash screen сжимается по ширине. Чтобы это исправить, переделаем splash screen следующим образом
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primaryColor"
tools:context=".splash.SplashActivity">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/fab_logo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Фрагменты и меню
Теперь добавим фрагменты и карту для нашего приложения. В activity_main добавим viewpager2 на весь экран
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".main.MainActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Далее создадим 4 фрагмента, которые будут листаться во ViewPager2 (MapFragment, ListFragment, NotificationsFragment, AccountFragment) и макеты к ним.
Перейдем в MainActivity и настроим pager и листание фрагментов. Сначала добавим адаптер и фрагменты для pager.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setNavigationBarColor(getColor(R.color.primaryColor));
setContentView(R.layout.activity_main);
ViewPager2 pager = findViewById(R.id.pager);
Adapter adapter = new Adapter(this);
adapter.addFragment(new MapFragment());
adapter.addFragment(new ListFragment());
adapter.addFragment(new NotificationsFragment());
adapter.addFragment(new AccountFragment());
pager.setAdapter(adapter);
pager.setCurrentItem(0);
pager.setUserInputEnabled(false);
}
private static class Adapter extends FragmentStateAdapter {
private final List<Fragment> list = new ArrayList<>();
public void addFragment(Fragment fragment) {
list.add(fragment);
}
public Adapter(FragmentActivity fa) {
super(fa);
}
@NonNull
@Override
public Fragment createFragment(int position) {
return list.get(position);
}
@Override
public int getItemCount() {
return list.size();
}
}
}
Далее настроим переключение пунктов меню
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setNavigationBarColor(getColor(R.color.primaryColor));
setContentView(R.layout.activity_main);
ImageButton mapButton = findViewById(R.id.map);
mapButton.setColorFilter(getColor(R.color.secondaryColor));
ImageButton listButton = findViewById(R.id.list);
ImageButton notificationsButton = findViewById(R.id.notifications);
ImageButton accountButton = findViewById(R.id.account);
mapButton.setOnClickListener(v -> {
mapButton.setColorFilter(getColor(R.color.secondaryColor));
listButton.setColorFilter(getColor(R.color.white));
notificationsButton.setColorFilter(getColor(R.color.white));
accountButton.setColorFilter(getColor(R.color.white));
pager.setCurrentItem(0,false);
});
listButton.setOnClickListener(v -> {
mapButton.setColorFilter(getColor(R.color.white));
listButton.setColorFilter(getColor(R.color.secondaryColor));
notificationsButton.setColorFilter(getColor(R.color.white));
accountButton.setColorFilter(getColor(R.color.white));
pager.setCurrentItem(1,false);
});
notificationsButton.setOnClickListener(v -> {
mapButton.setColorFilter(getColor(R.color.white));
listButton.setColorFilter(getColor(R.color.white));
notificationsButton.setColorFilter(getColor(R.color.secondaryColor));
accountButton.setColorFilter(getColor(R.color.white));
pager.setCurrentItem(2,false);
});
accountButton.setOnClickListener(v -> {
mapButton.setColorFilter(getColor(R.color.white));
listButton.setColorFilter(getColor(R.color.white));
notificationsButton.setColorFilter(getColor(R.color.white));
accountButton.setColorFilter(getColor(R.color.secondaryColor));
pager.setCurrentItem(3,false);
});
}
}
Карта
Переходим в Google Cloud Platform https://console.cloud.google.com/
Добавляем новый проект (APIs & Services) -> Overview -> Create Project
В Android Studio в SDK скачиваем google play services
Добавляем в build.gradle зависимость
implementation 'com.google.android.gms:play-services-maps:17.0.1'
Добавляем в файл манифеста разрешение на получение местоположения устройства и интернет
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET" />
Также в файл манифеста добавляем ссылку на ключ google api
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="ua.opu.give">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Give">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<activity android:name=".splash.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".main.MainActivity" />
</application>
</manifest>
В ресурсах values создаем ресурс google_maps_api.xml
, в котором добавляем ключ, полученный из Google Cloud Platform
В классе MapFragment добавляем код для гугл карт
public class MapFragment extends Fragment {
private OnMapReadyCallback callback = new OnMapReadyCallback() {
@Override
public void onMapReady(GoogleMap googleMap) {
LatLng odessa = new LatLng( 46.482952, 30.712481);
googleMap.addMarker(new MarkerOptions().position(odessa).title("Marker in Odessa"));
googleMap.moveCamera(CameraUpdateFactory.newLatLng(odessa));
}
};
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_map, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SupportMapFragment mapFragment =
(SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
if (mapFragment != null) {
mapFragment.getMapAsync(callback);
}
}
}
Макет фрагмента
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".main.MapFragment" />
AppRepository, ViewModel
Создаем синглтон AppRepository
public class AppRepository {
private static AppRepository instance;
public static AppRepository getInstance(Context context) {
if (instance == null) instance = new AppRepository(context);
return instance;
}
private AppRepository(Context context) {
}
}
Далее создаем MainViewModel для фрагментов MainActivity
public class MainViewModel extends AndroidViewModel {
private final AppRepository repository;
public MainViewModel(@NonNull Application application) {
super(application);
repository = AppRepository.getInstance(application);
}
}
Добавим класс Post с полям, конструктором и геттерами/сеттерами.
public class Post {
private Uri image;
private double latitude;
private double longitude;
private String header;
private String description;
}
Добавим в AppRepository список постов, а также публичный метод, который возвращает этот список.
public class AppRepository {
private List<Post> list;
private static AppRepository instance;
public static AppRepository getInstance(Context context) {
if (instance == null) instance = new AppRepository(context);
return instance;
}
private AppRepository(Context context) {
list = new ArrayList<>();
list.add(new Post(
new Uri.Builder().scheme("res").path(String.valueOf(R.drawable.give_example)).build(),
46.46177170826532, 30.746129175128416,
"Header text",
"Description"
));
}
public MutableLiveData<List<Post>> getList() {
return new MutableLiveData<>(list);
}
}
Далее модифицируем MainViewModel, добавляем метод getData(), который возвращает список данных из репозитория
public class MainViewModel extends AndroidViewModel {
private final AppRepository repository;
public MainViewModel(@NonNull Application application) {
super(application);
repository = AppRepository.getInstance(application);
}
public LiveData<List<Post>> getData() {
return repository.getList();
}
}
Далее подключаем ViewModel в фрагмент карты, а также добавляем показ маркера на карте
public class MapFragment extends Fragment {
private MainViewModel model;
private GoogleMap map;
private OnMapReadyCallback callback = new OnMapReadyCallback() {
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onMapReady(GoogleMap googleMap) {
map = googleMap;
map.getUiSettings().setCompassEnabled(false);
model.getData().getValue().forEach(MapFragment.this::addNewMarker);
}
};
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_map, container, false);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = ViewModelProviders.of(Objects.requireNonNull(getActivity())).get(MainViewModel.class);
model.getData().observe(getViewLifecycleOwner(), posts -> {
posts.forEach(this::addNewMarker);
});
SupportMapFragment mapFragment =
(SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
if (mapFragment != null) {
mapFragment.getMapAsync(callback);
}
}
public void addNewMarker(Post post) {
MarkerOptions markerOption = new MarkerOptions()
.position(new LatLng(post.getLatitude(), post.getLongitude()))
.snippet(post.getDescription())
.title(post.getHeader());
if (map != null) {
Marker marker = map.addMarker(markerOption);
}
}
}
Добавим еще один пост в репозиторий
private AppRepository(Context context) {
list = new ArrayList<>();
list.add(new Post(
new Uri.Builder().scheme("res").path(String.valueOf(R.drawable.give_example)).build(),
46.46177170826532, 30.746129175128416,
"Header text1",
"Description1"
));
list.add(new Post(
new Uri.Builder().scheme("res").path(String.valueOf(R.drawable.give_example)).build(),
46.469084426946466, 30.737365481594992,
"Header text2",
"Description2"
));
Далее добавим методы в фрагменты, которые добавляют новый маркер, список маркеров и клик на маркер
public class MapFragment extends Fragment implements GoogleMap.OnMarkerClickListener {
private MainViewModel model;
private GoogleMap map;
private HashMap<Marker, Post> markers = new HashMap<>();
private OnMapReadyCallback callback = new OnMapReadyCallback() {
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onMapReady(GoogleMap googleMap) {
map = googleMap;
map.setOnMarkerClickListener(MapFragment.this);
map.getUiSettings().setCompassEnabled(false);
invalidateMap();
}
};
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_map, container, false);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = ViewModelProviders.of(Objects.requireNonNull(getActivity())).get(MainViewModel.class);
model.getData().observe(getViewLifecycleOwner(), posts -> {
if (map == null)
return;
invalidateMap();
});
SupportMapFragment mapFragment =
(SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
if (mapFragment != null) {
mapFragment.getMapAsync(callback);
}
}
private void invalidateMap() {
clearMarkers();
model.getData().getValue().forEach(MapFragment.this::addNewMarker);
setActiveMarker(model.getData().getValue().get(0), true);
}
public void addNewMarker(Post post) {
MarkerOptions markerOption = new MarkerOptions()
.position(new LatLng(post.getLatitude(), post.getLongitude()))
.snippet(post.getDescription())
.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker_disabled))
.title(post.getHeader());
if (map != null) {
Marker marker = map.addMarker(markerOption);
markers.put(marker, post);
}
}
public void clearMarkers() {
if (map != null) {
map.clear();
markers.clear();
}
}
@Override
public boolean onMarkerClick(Marker marker) {
for (Map.Entry<Marker, Post> entry : markers.entrySet()) {
Marker m = entry.getKey();
Post post = entry.getValue();
if (m.getId().equals(marker.getId())) {
setActiveMarker(post, true);
return true;
}
}
return false;
}
public void setActiveMarker(Post post, boolean initFromMarker) {
for (Map.Entry<Marker, Post> entry : markers.entrySet()) {
Post p = entry.getValue();
Marker m = entry.getKey();
if (p.getLatitude() == post.getLatitude() && p.getLongitude() == post.getLongitude()) {
CameraUpdate center = CameraUpdateFactory.newLatLng(new LatLng(post.getLatitude(), post.getLongitude()));
CameraUpdate new_zoom = CameraUpdateFactory.zoomTo(map.getCameraPosition().zoom);
map.moveCamera(center);
map.animateCamera(new_zoom);
m.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker));
m.setZIndex(Float.MAX_VALUE);
} else {
m.setIcon(BitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker_disabled));
m.setZIndex(0f);
}
}
}
}
Last updated