First Steps

This commit is contained in:
TheBrokenRail 2024-02-16 22:04:50 -05:00
parent e79a35b90e
commit 1d98619a3b
29 changed files with 962 additions and 24 deletions

View File

@ -0,0 +1,117 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -24,6 +24,8 @@ android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
// Desugaring (Java 8+ APIs)
isCoreLibraryDesugaringEnabled = true
}
}
@ -31,4 +33,10 @@ dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
// OkHttp (For HTTP)
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// Moshi (For JSON)
implementation("com.squareup.moshi:moshi:1.15.0")
// Desugaring
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}

View File

@ -18,4 +18,7 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-keepclassmembers class com.thebrokenrail.mtudining.api.method.** {
*;
}

View File

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -14,7 +16,7 @@
tools:targetApi="31">
<activity
android:name=".activity.ListActivity"
android:name=".activity.list.ListActivity"
android:exported="true"
android:theme="@style/Theme.MTUDining">
<intent-filter>

View File

@ -1,20 +0,0 @@
package com.thebrokenrail.mtudining.activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.thebrokenrail.mtudining.R;
/**
* This activity lists the available food halls.
*/
public class ListActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Setup UI
setContentView(R.layout.activity_list);
}
}

View File

@ -0,0 +1,35 @@
package com.thebrokenrail.mtudining.activity.list;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.thebrokenrail.mtudining.R;
/**
* This activity lists the available food halls.
*/
public class ListActivity extends AppCompatActivity {
private ListAdapter adapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Setup UI
setContentView(R.layout.activity_list);
// Get View Model
ViewModelProvider viewModelProvider = new ViewModelProvider(this);
ListViewModel viewModel = viewModelProvider.get(ListViewModel.class);
// Setup RecyclerView
adapter = new ListAdapter(viewModel.task);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
}
}

View File

@ -0,0 +1,66 @@
package com.thebrokenrail.mtudining.activity.list;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.card.MaterialCardView;
import com.thebrokenrail.mtudining.activity.task.Task;
import com.thebrokenrail.mtudining.activity.task.TaskAdapter;
import com.thebrokenrail.mtudining.widget.CategoryView;
import com.thebrokenrail.mtudining.widget.LoaderView;
/**
* Adapter for listing dining halls.
*/
public class ListAdapter extends TaskAdapter<ListData, ListAdapter.ViewHolder> {
public static class ViewHolder extends RecyclerView.ViewHolder {
private final CategoryView view;
private ViewHolder(@NonNull View itemView) {
super(itemView);
view = (CategoryView) itemView;
}
}
public ListAdapter(Task<ListData> task) {
super(task);
}
@Override
protected ViewHolder createItemViewHolder(@NonNull ViewGroup parent) {
// Create View
CategoryView category = new CategoryView(parent.getContext(), null);
RecyclerView.LayoutParams layoutParams = new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT);
category.setLayoutParams(layoutParams);
return new ViewHolder(category);
}
@Override
protected void bindDataViewHolder(@NonNull ViewHolder holder, int position) {
ListData.Category data = getResult().categories.get(position);
// Setup View
holder.view.setup(data.isOpen, data.name, () -> {
// Open/Close Category
data.isOpen = !data.isOpen;
notifyItemChanged(getResult().categories.indexOf(data));
});
// Add Locations
holder.view.children.removeAllViews();
for (ListData.Element location : data.locations) {
AppCompatTextView textView = new AppCompatTextView(holder.view.getContext());
textView.setText(location.name);
MaterialCardView.LayoutParams innerLayoutParams = new MaterialCardView.LayoutParams(MaterialCardView.LayoutParams.MATCH_PARENT, MaterialCardView.LayoutParams.WRAP_CONTENT);
textView.setLayoutParams(innerLayoutParams);
holder.view.children.addView(textView);
}
}
@Override
protected int getDataSize() {
return getResult().categories.size();
}
}

View File

@ -0,0 +1,36 @@
package com.thebrokenrail.mtudining.activity.list;
import java.util.ArrayList;
import java.util.List;
/**
* Data to be displayed in {@link ListActivity}.
*/
public class ListData {
public static class Element {
public final String id;
public final String name;
public Element(String id, String name) {
this.id = id;
this.name = name;
}
}
public static class Category {
public final String name;
public boolean isOpen = true;
public final List<Element> locations = new ArrayList<>();
public Category(String name) {
this.name = name;
}
}
public final String siteId;
public final List<Category> categories = new ArrayList<>();
public ListData(String siteId) {
this.siteId = siteId;
}
}

View File

@ -0,0 +1,52 @@
package com.thebrokenrail.mtudining.activity.list;
import androidx.lifecycle.ViewModel;
import com.thebrokenrail.mtudining.activity.task.Task;
import com.thebrokenrail.mtudining.api.Connection;
import com.thebrokenrail.mtudining.api.method.AllLocations;
import com.thebrokenrail.mtudining.api.method.Info;
import com.thebrokenrail.mtudining.util.Constants;
/**
* Data preserved between screen rotations for {@link ListActivity}.
*/
public class ListViewModel extends ViewModel {
private final Connection connection = new Connection();
public final Task<ListData> task = new Task<ListData>() {
@Override
protected void startImpl() {
// Load Site Info
Info info = new Info();
connection.send(info, infoResponse -> {
// Load Locations
AllLocations allLocations = new AllLocations(Constants.PLATFORM, infoResponse.site.id, true, false, true);
connection.send(allLocations, allLocationsResponse -> {
// Success
ListData data = new ListData(infoResponse.site.id);
// Find Active Locations
for (AllLocations.Response.Building building : allLocationsResponse.buildings) {
if (building.active) {
// Found Active Building
ListData.Category category = new ListData.Category(building.name);
for (AllLocations.Response.Location location : building.locations) {
if (location.active) {
// Found Active Location
category.locations.add(new ListData.Element(location.id, location.name));
}
}
data.categories.add(category);
}
}
done(data);
}, () -> {
// Failed Fetching Location Info
done(null);
});
}, () -> {
// Failed Fetching Site Info
done(null);
});
}
};
}

View File

@ -0,0 +1,111 @@
package com.thebrokenrail.mtudining.activity.task;
import java.util.ArrayList;
import java.util.List;
/**
* An asynchronous task (usually a network call).
*/
public abstract class Task<E> {
/**
* Current status of the task.
*/
public enum Status {
NOT_STARTED,
IN_PROGRESS,
DONE,
ERROR
}
private Status status = Status.NOT_STARTED;
/**
* Start task.
*/
public void start() {
status = Status.IN_PROGRESS;
startImpl();
// Update Listeners
sendDataToListeners();
}
/**
* Implementation of {@link #start()}.
*/
protected abstract void startImpl();
/**
* Currently stored result (or null if none).
*/
private E result = null;
/**
* Store result.
* @param obj Result (or null if error)
*/
protected void done(E obj) {
result = obj;
// Update Status
if (obj == null) {
// Error
status = Status.ERROR;
} else {
// Done
status = Status.DONE;
}
// Update Listeners
sendDataToListeners();
}
/**
* Event listener.
*/
public interface Listener<E> {
/**
* Handle task update.
* @param status The task's status
* @param result The task's result (or null if none)
*/
void update(Status status, E result);
}
/**
* Stored event listeners.
*/
private final List<Listener<E>> listeners = new ArrayList<>();
/**
* Add new listener and send existing data to it.
* @param listener The new listener
*/
public void addListener(Listener<E> listener) {
if (!listeners.contains(listener)) {
listeners.add(listener);
// Send Data
sendDataToListener(listener);
}
}
/**
* Send data to specific listener.
* @param listener The listener
*/
private void sendDataToListener(Listener<E> listener) {
listener.update(status, status == Status.DONE ? result : null);
}
/**
* Send data to all listeners.
*/
private void sendDataToListeners() {
for (Listener<E> listener : listeners) {
sendDataToListener(listener);
}
}
/**
* Remove listener.
* @param listener The listener to remove
*/
public void removeListener(Listener<E> listener) {
listeners.remove(listener);
}
}

View File

@ -0,0 +1,163 @@
package com.thebrokenrail.mtudining.activity.task;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.thebrokenrail.mtudining.widget.LoaderView;
/**
* RecyclerView adapter for a given {@link Task}.
*/
public abstract class TaskAdapter<E, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements Task.Listener<E> {
public static final int LOADER_TYPE = 1;
public static final int DATA_TYPE = 0;
/**
* The task.
*/
private final Task<E> task;
/**
* Current task status.
*/
private Task.Status currentStatus;
/**
* Task result (or null if none).
*/
private E result;
/**
* Get task result.
* @return Task result
*/
protected E getResult() {
return result;
}
/**
* Create adapter.
* @param task The task
*/
public TaskAdapter(Task<E> task) {
// Setup Event Handling
this.task = task;
task.addListener(this);
}
/**
* Call when activity is destroyed.
*/
public void onDestroy() {
// Stop Handling Events
task.removeListener(this);
}
/**
* Create view holder for item in task's result.
* @param parent The view parent
* @return The new view holder
*/
protected abstract VH createItemViewHolder(@NonNull ViewGroup parent);
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == LOADER_TYPE) {
// Loader
LoaderView loader = new LoaderView(parent.getContext(), null);
RecyclerView.LayoutParams layoutParams = new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT);
loader.setLayoutParams(layoutParams);
return new RecyclerView.ViewHolder(loader) {};
} else if (viewType == DATA_TYPE) {
// Data
return createItemViewHolder(parent);
} else {
// Unknown
throw new RuntimeException();
}
}
/**
* Bind view holder for item in task's result.
* @param holder The view holder
* @param position The item's position
*/
protected abstract void bindDataViewHolder(@NonNull VH holder, int position);
@SuppressWarnings({"unchecked"})
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
int type = getItemViewType(position);
if (type == LOADER_TYPE) {
// Loader
((LoaderView) holder.itemView).setup(currentStatus == Task.Status.ERROR, task::start);
} else if (type == DATA_TYPE) {
// Data
bindDataViewHolder((VH) holder, position);
}
}
/**
* Get size of task's dataset.
* @return The size of the dataset
*/
protected abstract int getDataSize();
@Override
public int getItemCount() {
if (currentStatus != Task.Status.DONE) {
// Only Loader
return 1;
} else {
// Data Size
return getDataSize();
}
}
@Override
public int getItemViewType(int position) {
if (currentStatus != Task.Status.DONE && position == 0) {
// Loader
return LOADER_TYPE;
} else {
// Data
return DATA_TYPE;
}
}
@Override
public void update(Task.Status status, E result) {
int oldItemCount = getItemCount();
// Store
this.currentStatus = status;
this.result = result;
// Change UI
if (status == Task.Status.IN_PROGRESS || status == Task.Status.NOT_STARTED) {
// Show Progress Circle
reloadUI(oldItemCount);
// Start Task If Needed
if (status == Task.Status.NOT_STARTED) {
task.start();
}
} else if (status == Task.Status.ERROR) {
// Show Error
reloadUI(oldItemCount);
} else {
// Done
reloadUI(oldItemCount);
}
}
/**
* Reload UI.
*/
private void reloadUI(int oldItemCount) {
// Remove Existing Items
notifyItemRangeRemoved(0, oldItemCount);
// Add Items
notifyItemRangeInserted(0, getItemCount());
}
}

View File

@ -0,0 +1,90 @@
package com.thebrokenrail.mtudining.api;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.thebrokenrail.mtudining.util.Constants;
import java.io.IOException;
import java.util.function.Consumer;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class Connection {
/**
* HTTP Client
*/
private final OkHttpClient client = new OkHttpClient();
/**
* Link to main thread.
*/
private final Handler mainThread = new Handler(Looper.getMainLooper());
/**
* Send API method.
* @param method API method
* @param success Callback on success
* @param error Callback on error
* @param <T> Response type
*/
public <T> void send(Method<T> method, Consumer<T> success, Runnable error) {
// Build URL
String url = Constants.API_BASE + method.getPath();
// Build Request
Request request = new Request.Builder()
.url(url)
.build();
// Call
client.newCall(request).enqueue(new Callback() {
private void error(Exception e) {
// Print Error
e.printStackTrace();
// Callback
mainThread.post(error);
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
error(e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
try {
try (ResponseBody responseBody = response.body()) {
// Check Response
if (!response.isSuccessful()) {
// Run Error Callback
mainThread.post(error);
return;
}
// Deserialize Body
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<T> jsonAdapter = moshi.adapter(method.getResponseClass());
assert responseBody != null;
T obj = jsonAdapter.fromJson(responseBody.string());
// Run Callback
mainThread.post(() -> success.accept(obj));
}
} catch (Exception e) {
// Run Error Callback
error(e);
}
}
});
}
}

View File

@ -0,0 +1,14 @@
package com.thebrokenrail.mtudining.api;
public interface Method<T> {
/**
* Get the API method's path.
* @return The path
*/
String getPath();
/**
* Get the response class. Needed for deserialization.
* @return The response class
*/
Class<T> getResponseClass();
}

View File

@ -0,0 +1,48 @@
package com.thebrokenrail.mtudining.api.method;
import com.thebrokenrail.mtudining.api.Method;
import java.util.List;
public class AllLocations implements Method<AllLocations.Response> {
private final int platform;
private final String siteId;
private final boolean forMenus;
private final boolean withAddress;
private final boolean withBuildings;
public AllLocations(int platform, String siteId, boolean forMenus, boolean withAddress, boolean withBuildings) {
this.platform = platform;
this.siteId = siteId;
this.forMenus = forMenus;
this.withAddress = withAddress;
this.withBuildings = withBuildings;
}
@Override
public String getPath() {
return "/locations/all_locations?platform=" + platform + "&site_id=" + siteId + "&for_menus=" + forMenus + "&with_address=" + withAddress + "&with_buildings=" + withBuildings;
}
@Override
public Class<Response> getResponseClass() {
return Response.class;
}
public static class Response {
public static class Building {
public String id;
public String name;
public boolean active;
public boolean show_menus;
public List<Location> locations;
}
public List<Building> buildings;
public static class Location {
public String id;
public String name;
public boolean active;
}
public List<Location> locations;
}
}

View File

@ -0,0 +1,26 @@
package com.thebrokenrail.mtudining.api.method;
import com.thebrokenrail.mtudining.api.Method;
import com.thebrokenrail.mtudining.util.Constants;
public class Info implements Method<Info.Response> {
@Override
public String getPath() {
return "/sites/" + Constants.SITE_NAME + "/info";
}
@Override
public Class<Response> getResponseClass() {
return Response.class;
}
// This class contains a lot of data,
// I'm only including the fields needed
// for the app.
public static class Response {
public static class Site {
public String id;
}
public Site site;
}
}

View File

@ -0,0 +1,7 @@
package com.thebrokenrail.mtudining.util;
public class Constants {
public final static String API_BASE = "https://api.dineoncampus.com/v1";
public final static String SITE_NAME = "MTU";
public final static int PLATFORM = 0;
}

View File

@ -0,0 +1,67 @@
package com.thebrokenrail.mtudining.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import com.google.android.material.card.MaterialCardView;
import com.thebrokenrail.mtudining.R;
/**
* Widget that shows a category of items.
*/
public class CategoryView extends FrameLayout {
private final LinearLayout inner;
public final MaterialCardView children;
private final AppCompatTextView title;
public CategoryView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
inner = new LinearLayout(context);
inner.setOrientation(LinearLayout.VERTICAL);
addView(inner);
// Set Margin
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
int margin = getResources().getDimensionPixelSize(R.dimen.margin);
layoutParams.setMargins(margin, margin, margin, margin);
inner.setLayoutParams(layoutParams);
// Add Title
title = new AppCompatTextView(context);
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(androidx.appcompat.R.attr.colorPrimary, value, true);
title.setTextColor(value.data);
title.setClickable(true);
title.setFocusable(true);
inner.addView(title);
LinearLayout.LayoutParams innerLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
title.setLayoutParams(innerLayoutParams);
// Setup Children
children = new MaterialCardView(context, null, com.google.android.material.R.attr.materialCardViewElevatedStyle);
inner.addView(children);
innerLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
innerLayoutParams.setMargins(0, margin, 0, 0);
children.setLayoutParams(innerLayoutParams);
}
/**
* Setup widget.
* @param isOpen If category is open
* @param titleText The category's title
* @param onClickTitle Callback when clicking on title
*/
public void setup(boolean isOpen, String titleText, Runnable onClickTitle) {
titleText = (isOpen ? "" : "") + " " + titleText;
title.setText(titleText);
children.setVisibility(isOpen ? VISIBLE : GONE);
title.setOnClickListener(v -> onClickTitle.run());
}
}

View File

@ -0,0 +1,88 @@
package com.thebrokenrail.mtudining.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.thebrokenrail.mtudining.R;
/**
* Widget that shows a progress indicator for the loading of data.
*/
public class LoaderView extends LinearLayout {
private final LinearLayout inner;
public LoaderView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
inner = new LinearLayout(context);
inner.setGravity(Gravity.CENTER_HORIZONTAL);
inner.setOrientation(LinearLayout.VERTICAL);
addView(inner);
setup(false, null);
// Set Margin
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
int margin = getResources().getDimensionPixelSize(R.dimen.margin);
layoutParams.setMargins(margin, margin, margin, margin);
inner.setLayoutParams(layoutParams);
}
/**
* Setup widget.
* @param isError If the error message should be shown
* @param retry Callback that is executed when the retry button is clicked
*/
public void setup(boolean isError, Runnable retry) {
if (!isError) {
setupProgress();
} else {
setupError(retry);
}
}
/**
* Display progress widget.
*/
private void setupProgress() {
inner.removeAllViews();
// Create Progress Indicator
CircularProgressIndicator progressIndicator = new CircularProgressIndicator(getContext());
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
progressIndicator.setLayoutParams(layoutParams);
progressIndicator.setIndeterminate(true);
inner.addView(progressIndicator);
}
/**
* Display error.
* @param retry Callback that is executed when the retry button is clicked
*/
private void setupError(Runnable retry) {
inner.removeAllViews();
// Create TextView
TextView text = new TextView(getContext());
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
text.setLayoutParams(layoutParams);
text.setTextAlignment(TEXT_ALIGNMENT_CENTER);
text.setText(R.string.load_error);
inner.addView(text);
// Create Retry Button
Button button = new MaterialButton(getContext());
int margin = getResources().getDimensionPixelSize(R.dimen.margin);
layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(0, margin, 0, 0);
button.setLayoutParams(layoutParams);
button.setText(R.string.retry);
button.setOnClickListener(v -> retry.run());
inner.addView(button);
}
}

View File

@ -5,7 +5,7 @@
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
tools:context=".activity.ListActivity">
tools:context=".activity.list.ListActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
@ -23,6 +23,7 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Material Design -->
<color name="md_theme_primary">#E0C56D</color>

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ThemeOverlay.AppTheme.MediumContrast" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/md_theme_primary_mediumContrast</item>

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="Base.Theme.MTUDining" parent="Theme.Material3.DayNight.NoActionBar">

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.MTUDining" parent="Base.Theme.MTUDining">
<!-- Transparent system bars for edge-to-edge. -->

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="margin">8dp</dimen>
</resources>

View File

@ -1,3 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Dining[MTU]</string>
<string name="retry">Retry</string>
<string name="load_error">Unable to load data!</string>
</resources>

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ThemeOverlay.AppTheme.MediumContrast" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/md_theme_primary_mediumContrast</item>

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="Base.Theme.MTUDining" parent="Theme.Material3.DayNight.NoActionBar">