From db2b1e081d78162bc02e1384a7779a3a788b0b84 Mon Sep 17 00:00:00 2001 From: Jack Shendrikov Date: Wed, 17 Feb 2021 13:20:33 +0200 Subject: [PATCH] Add Lab6 --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 4 +- .../io8227/jackshen/AboutBookActivity.java | 1 + .../comsys/io8227/jackshen/BookActivity.java | 243 ++++--- .../comsys/io8227/jackshen/BookAdapter.java | 3 + .../io8227/jackshen/BookJSONParser.java | 293 +++++--- .../comsys/io8227/jackshen/BookLoader.java | 77 +++ .../io8227/jackshen/GalleryActivity.java | 93 --- .../io8227/jackshen/HttpRequestImgHelper.java | 92 +++ .../comsys/io8227/jackshen/MainActivity.java | 2 +- .../kpi/comsys/io8227/jackshen/Picture.java | 17 + .../io8227/jackshen/PictureActivity.java | 367 ++++++++++ .../io8227/jackshen/PictureAdapter.java | 95 +++ .../io8227/jackshen/PictureJSONParser.java | 202 ++++++ .../comsys/io8227/jackshen/PictureLoader.java | 77 +++ .../io8227/jackshen/PictureViewHolder.java | 17 + .../jackshen/SpannedGridLayoutManager.java | 630 ++++++++++++++++++ .../{ => coordinateJS}/CoordinateJS.java | 8 +- .../jackshen/{ => coordinateJS}/Latitude.java | 8 +- .../{ => coordinateJS}/Longitude.java | 8 +- .../calculator/DistanceCalculator.java | 16 +- .../coordinate/AbsGeoCoordinate.java | 8 +- .../coordinate/GeoCoordinate.java | 2 +- .../coordinate/LatLngDirection.java | 2 +- .../exception/GeoCoordException.java | 2 +- app/src/main/res/drawable/grid.png | Bin 0 -> 493 bytes app/src/main/res/drawable/list.png | Bin 0 -> 627 bytes app/src/main/res/drawable/rect_background.xml | 10 + app/src/main/res/layout/activity_book.xml | 26 +- app/src/main/res/layout/activity_gallery.xml | 227 +++---- app/src/main/res/layout/book_list_item.xml | 4 +- app/src/main/res/layout/image_item.xml | 21 + app/src/main/res/values/attr.xml | 8 + app/src/main/res/values/dimen.xml | 1 + .../coordinateJS}/CoordinateJSTest.java | 8 +- .../coordinateJS}/DistanceCalculatorTest.java | 13 +- .../coordinateJS}/LatitudeTest.java | 6 +- .../coordinateJS}/LongitudeTest.java | 6 +- 38 files changed, 2125 insertions(+), 475 deletions(-) create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookLoader.java delete mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/GalleryActivity.java create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/HttpRequestImgHelper.java create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/Picture.java create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureActivity.java create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureAdapter.java create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureJSONParser.java create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureLoader.java create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureViewHolder.java create mode 100644 app/src/main/java/ua/kpi/comsys/io8227/jackshen/SpannedGridLayoutManager.java rename app/src/main/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS}/CoordinateJS.java (92%) rename app/src/main/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS}/Latitude.java (92%) rename app/src/main/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS}/Longitude.java (92%) rename app/src/main/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS}/calculator/DistanceCalculator.java (88%) rename app/src/main/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS}/coordinate/AbsGeoCoordinate.java (96%) rename app/src/main/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS}/coordinate/GeoCoordinate.java (74%) rename app/src/main/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS}/coordinate/LatLngDirection.java (50%) rename app/src/main/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS}/exception/GeoCoordException.java (96%) create mode 100644 app/src/main/res/drawable/grid.png create mode 100644 app/src/main/res/drawable/list.png create mode 100644 app/src/main/res/drawable/rect_background.xml create mode 100644 app/src/main/res/layout/image_item.xml create mode 100644 app/src/main/res/values/attr.xml rename app/src/test/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS/calculator/coordinateJS}/CoordinateJSTest.java (90%) rename app/src/test/java/ua/kpi/comsys/io8227/jackshen/{calculator => coordinateJS/calculator/coordinateJS}/DistanceCalculatorTest.java (87%) rename app/src/test/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS/calculator/coordinateJS}/LatitudeTest.java (95%) rename app/src/test/java/ua/kpi/comsys/io8227/jackshen/{ => coordinateJS/calculator/coordinateJS}/LongitudeTest.java (95%) diff --git a/app/build.gradle b/app/build.gradle index da4f03c..5ac4240 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,7 +32,10 @@ dependencies { implementation 'com.jackandphantom.android:customtogglebutton:1.0.1' implementation 'com.github.PhilJay:MPAndroidChart:v3.0.3' implementation 'com.jjoe64:graphview:4.2.2' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'org.lucasr.twowayview:twowayview:0.1.4' implementation 'com.baoyz.swipemenulistview:library:1.3.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e09c6d..ffe7ff6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -89,11 +89,11 @@ - - + diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/AboutBookActivity.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/AboutBookActivity.java index 06770c6..e11cd94 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/AboutBookActivity.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/AboutBookActivity.java @@ -16,6 +16,7 @@ protected void onCreate(Bundle savedInstanceState) { Book fullBook = (Book) getIntent().getSerializableExtra("book_full"); ImageView cover = findViewById(R.id.image_full); + assert fullBook != null; if (fullBook.getImageUrl().equals("")) { cover.setImageResource(R.drawable.noimage); } else { diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookActivity.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookActivity.java index d6a5f97..51530a2 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookActivity.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookActivity.java @@ -1,20 +1,21 @@ package ua.kpi.comsys.io8227.jackshen; +import android.app.LoaderManager; import android.app.Activity; +import android.content.Context; import android.content.Intent; -import android.graphics.Outline; +import android.content.Loader; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.Build; import android.os.Bundle; -import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.ViewOutlineProvider; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; -import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; @@ -29,18 +30,28 @@ import com.baoyz.swipemenulistview.SwipeMenuListView; import com.google.android.material.textfield.TextInputEditText; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; +import java.util.Objects; -public class BookActivity extends AppCompatActivity { +public class BookActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks> { + + /** URL for book data */ + public static final String REQUEST_URL = ""; + + /** + * Constant value for the book loader ID. + * We can choose any int, it`s really needed for multiple loaders + */ + private static final int BOOK_LOADER_ID = 1; /** Adapter for the list of books */ private BookAdapter mAdapter; /** Handler for the list of books */ - List books; + List books = new ArrayList<>(); /** InputText for the book search */ private TextInputEditText mSearchText; @@ -48,6 +59,16 @@ public class BookActivity extends AppCompatActivity { /** ImageButton for the book search */ private ImageView mSearchButton; + /** TextView that is displayed when the list is empty */ + private TextView mEmptyTextView; + + /** Loading indicator */ + private View mLoadingIndicator; + + private LoaderManager mLoaderManager; + + private String mQueryText; + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override protected void onCreate(Bundle savedInstanceState) { @@ -61,14 +82,18 @@ protected void onCreate(Bundle savedInstanceState) { // Setup UI to hide soft keyboard when clicked outside the {@link EditText} setupUI(findViewById(R.id.main_parent)); - // Load Data from JSON files - books = BookJSONParser.extractDataFromJson(this); - // Find a reference to the {@link bookListView} in the layout SwipeMenuListView bookListView = findViewById(R.id.list); + mLoadingIndicator = findViewById(R.id.progress_bar); + mLoadingIndicator.setVisibility(View.GONE); + + if (savedInstanceState != null) { + mQueryText = savedInstanceState.getString(REQUEST_URL); + } + // Set empty view when there is no data - View mEmptyTextView = findViewById(R.id.empty_view); + mEmptyTextView = findViewById(R.id.empty_view); bookListView.setEmptyView(mEmptyTextView); // Create a new adapter that takes an empty list of books as input @@ -106,7 +131,7 @@ public void create(SwipeMenu menu) { mSearchButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onClickSearch(); + onClickSearch(Objects.requireNonNull(mSearchText.getText()).toString()); } }); @@ -119,7 +144,8 @@ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_DONE) { // User has finished entering text // Perform the search button click programmatically - onClickSearch(); + onClickSearch(Objects.requireNonNull(mSearchText.getText()).toString()); + // Return true on successfully handling the action return true; } @@ -157,112 +183,74 @@ public void onItemClick(AdapterView adapterView, View view, int position, lon } }); - // Check if the book list has been updated, if so, add new values ​​to the activity - updateListView(); - - Button mAddButton = findViewById(R.id.buttonAdd); - //Stylization button for adding books - ViewOutlineProvider viewOutlineProvider = new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - int shapeSize = getResources().getDimensionPixelSize(R.dimen.shape_size); - outline.setRoundRect(0, 0, shapeSize, shapeSize, shapeSize / 2); - } - }; - mAddButton.setOutlineProvider(viewOutlineProvider); - mAddButton.setClipToOutline(true); + /* + * Initialize the loader + */ + if (mQueryText != null) { + getLoaderManager().initLoader(BOOK_LOADER_ID, null, this); + } } + /** This method is called when the user hits the search button */ - public void onClickSearch() { - // Get a handle for the editable text view holding the user's search text and convert it - // to lowercase string value - String text = mSearchText.getText().toString().toLowerCase(); - - // Create empty list of Books - List booksSearch = new ArrayList<>(); - - if (text.length() == 0) { - // User has not entered any search text - // Notify user to enter text via toast - Toast toast = Toast.makeText(getApplicationContext(), "Please, enter text", Toast.LENGTH_SHORT); - toast.setGravity(Gravity.CENTER, 0, 0); - toast.show(); - } else { - // Go through all books in the current list and search if any title contains the text - // we are looking for - for (int i = 0; i < mAdapter.getCount(); i++) { - if (mAdapter.getItem(i).getTitle() != null) { - // Get lowercase value of current book title - String temp = mAdapter.getItem(i).getTitle().toLowerCase(); - // If title contains our text -> create new Book item and add it to {@link booksSearch} - if (temp.contains(text)) { - Book findBook = new Book(mAdapter.getItem(i).getTitle(), - mAdapter.getItem(i).getSubtitle(), mAdapter.getItem(i).getAuthor(), - mAdapter.getItem(i).getPublisher(), mAdapter.getItem(i).getISBN(), - mAdapter.getItem(i).getPages(), mAdapter.getItem(i).getYear(), - mAdapter.getItem(i).getRate(), mAdapter.getItem(i).getDescription(), - mAdapter.getItem(i).getPrice(), mAdapter.getItem(i).getImageUrl()); - booksSearch.add(findBook); + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void onClickSearch(String searchText) { + mSearchText.clearFocus(); + mAdapter.clear(); + mEmptyTextView.setVisibility(View.GONE); + + // Get a reference to the ConnectivityManager to check state of network connectivity + ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + + // Get details on the currently active default data network + NetworkInfo networkInfo = Objects.requireNonNull(connMgr).getActiveNetworkInfo(); + + try { + // If there is a network connection -> get data + if (networkInfo != null && networkInfo.isConnected()) { + if (!searchText.isEmpty() && searchText.length() >= 3 && searchText != null) { + String bookName = URLEncoder.encode(searchText.trim().replaceAll(" ", "%20"), "UTF-8"); + + // Set the URL with the suitable bookName + mQueryText = "https://api.itbook.store/1.0/search/" + bookName; + + // Show the loading indicator. + mLoadingIndicator.setVisibility(View.VISIBLE); + + // Create a bundle called queryBundle + Bundle queryBundle = new Bundle(); + + // Use putString with REQUEST_URL as the key and the String value of the URL as the value + queryBundle.putString(REQUEST_URL, mQueryText); + + // Get a reference to the LoaderManager, in order to interact with loaders. + mLoaderManager = getLoaderManager(); + + // Get the loader initialized. Go through the above specified int ID constant + // and pass the bundle to null. + Loader BooksSearchLoader = mLoaderManager.getLoader(BOOK_LOADER_ID); + if (BooksSearchLoader == null) { + mLoaderManager.initLoader(BOOK_LOADER_ID, queryBundle, BookActivity.this); + } else { + mLoaderManager.restartLoader(BOOK_LOADER_ID, queryBundle, BookActivity.this); } + } else { + Toast.makeText(this, "You need to introduce some text to search (>=3 symbols)", Toast.LENGTH_LONG).show(); } - } - if (mAdapter.isEmpty()) { - // No results available - Toast toast = Toast.makeText(getApplicationContext(), "There are no results", Toast.LENGTH_SHORT); - toast.setGravity(Gravity.CENTER, 0, 0); - toast.show(); } else { - SwipeMenuListView bookListView = findViewById(R.id.list); - - // Sort all books by title - Collections.sort(booksSearch, new Comparator() { - @Override public int compare(Book u1, Book u2) { - return u1.getTitle().compareTo(u2.getTitle()); - } - }); - - // Create a new adapter that takes an empty list of books as input - mAdapter = new BookAdapter(this, booksSearch); - - // Set the adapter on the {@link bookListView} so the list can be populated in UI - bookListView.setAdapter(mAdapter); + // Otherwise, display error + // First, hide loading indicator so error message will be visible + mLoadingIndicator.setVisibility(View.INVISIBLE); - // Notify about changes - mAdapter.notifyDataSetChanged(); + // Update empty state with no connection error message + Toast.makeText(BookActivity.this, "No Internet Connection", Toast.LENGTH_SHORT).show(); + mEmptyTextView.setText("No Internet Connection"); } - } - } - - /** This method is called when the user hits the add button */ - public void onClickAdd(View view) { - Intent intent = new Intent(this, AddBookActivity.class); - startActivity(intent); - } - - /** - * This method is called when the user create new Book item and we need to add it to our - * books List - */ - public void updateListView() { - Bundle bundle = getIntent().getExtras(); - if (bundle != null) { - books.add(new Book(getIntent().getStringExtra("title_new"), - getIntent().getStringExtra("subtitle_new"), - "Unknown", - "Unknown", - getIntent().getStringExtra("isbn_new"), - "undefined", - "undefined", - getIntent().getStringExtra("rate_new"), - "", - "$" + getIntent().getStringExtra("price_new"), - "" - )); - mAdapter.notifyDataSetChanged(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); } } @@ -317,5 +305,42 @@ public static void hideSoftKeyboard(Activity activity) { } } + @Override + public Loader> onCreateLoader(int i, Bundle args) { + return new BookLoader(this, args); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + // Set empty text to display "No books found." + mEmptyTextView.setText("No books found"); + mEmptyTextView.setVisibility(View.VISIBLE); + + // Hide the indicator after the data is appeared + mLoadingIndicator.setVisibility(View.GONE); + + // Clear the adapter pf previous book data + mAdapter.clear(); + + // If there is a valid list of {@link Book}s, then add them to the adapter's + // data set. This will trigger the ListView to update + if (data != null && !data.isEmpty()) { + mAdapter.addAll(data); + } + + } + + @Override + public void onLoaderReset(Loader> loader) { + // Clear existing data on adapter as loader is reset + mAdapter.clear(); + } + + /** Save the data about url */ + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(REQUEST_URL, mQueryText); + } } diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookAdapter.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookAdapter.java index 1f01a20..28a1818 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookAdapter.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookAdapter.java @@ -78,6 +78,7 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { // Set the color on the rating circle ratingCircle.setColor(ratingColor); + // Find the TextView with view ID titleBook TextView titleView = listItemView.findViewById(R.id.titleBook); @@ -155,6 +156,8 @@ private String formatRating(double rating) { return ratingFormat.format(rating); } + + /** Class to download an image from URL */ public static class DownloadImage extends AsyncTask { final ImageView bmImage; diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookJSONParser.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookJSONParser.java index 0d9a561..2cb9235 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookJSONParser.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookJSONParser.java @@ -1,8 +1,12 @@ package ua.kpi.comsys.io8227.jackshen; -import android.content.Context; +import android.os.Build; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.RequiresApi; + +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -10,26 +14,17 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** Helper methods to request and retrieve book data from a specified JSON */ final class BookJSONParser { - private static int[] booksData = { - R.raw._9780321856715, - R.raw._9780321862969, - R.raw._9781118841471, - R.raw._9781430236054, - R.raw._9781430237105, - R.raw._9781430238072, - R.raw._9781430245124, - R.raw._9781430260226, - R.raw._9781449308360, - R.raw._9781449342753 - }; - /** Tag for the log messages */ private static final String LOG_MSG = BookJSONParser.class.getSimpleName(); @@ -37,108 +32,250 @@ final class BookJSONParser { * We are creating a private constructor because no one else should create * the {@link BookJSONParser} object. */ - private BookJSONParser() { } + private BookJSONParser() { + } + + /** + * Query given JSON and return a list of {@link String} objects. + * + * @param requestUrl - our URL as a {@link String} object + * @return the list of {@link Book}s + */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + static List getBookData(String requestUrl) { + // Create URL object + URL url = makeUrl(requestUrl); + // Perform HTTP request to the URL and receive a JSON response back + String jsonResponse = null; + try { + jsonResponse = makeHttpRequest(url); + } catch (IOException err) { + Log.e(LOG_MSG, "Problem making the HTTP request.", err); + } + + // Extract relevant fields from the JSON response and create a list of {@link Book}s + // Then return the list of {@link Book}s + return extractDataFromJson(jsonResponse); + } + + /** Returns new URL object from the given string URL. */ + private static URL makeUrl(String strUrl) { + // Initialize an empty {@link URL} object to hold the parsed URL from the strUrl + URL url = null; + + // Parse valid URL from param strUrl and handle Malformed urls + try { + url = new URL(strUrl); + } catch (MalformedURLException err) { + Log.e(LOG_MSG, "Problem building the URL!", err); + } + + // Return valid URL + return url; + } + + /** Make an HTTP request to the given URL and return a String as the response. */ + private static String makeHttpRequest(URL url) throws IOException { + // Initialize variable to hold the parsed JSON response + String jsonResponse = ""; + + // Return response if URL is null + if (url == null) { + return jsonResponse; + } + + // Initialize HTTP connection object + HttpURLConnection connection = null; + + // Initialize {@link InputStream} to hold response from request + InputStream inputStream = null; + try { + // Establish connection to the URL + connection = (HttpURLConnection) url.openConnection(); + + // Set request type + connection.setRequestMethod("GET"); + + // Setting how long to wait on the request (in milliseconds) + connection.setReadTimeout(10000); + connection.setConnectTimeout(15000); + + // Establish connection to the URL + connection.connect(); + + // Check for successful connection + if (connection.getResponseCode() == 200) { + inputStream = connection.getInputStream(); + jsonResponse = readFromStream(inputStream); + } else { + Log.e(LOG_MSG, "Error response code: " + connection.getResponseCode()); + } + } catch (IOException err) { + Log.e(LOG_MSG, "Problem retrieving the book JSON results.", err); + } finally { + if (connection != null) { + // Disconnect the connection after successfully making the HTTP request + connection.disconnect(); + } + + if (inputStream != null) { + // Close the stream after successfully parsing the request + // This also can throw an IOException + inputStream.close(); + } + } + + // Return JSON as a {@link String} + return jsonResponse; + } /** * Convert the {@link InputStream} into a String which contains the whole JSON response. */ - private static String readFromStream(Context context, int resID) throws IOException { + private static String readFromStream(InputStream inputStream) throws IOException { StringBuilder output = new StringBuilder(); + if (inputStream != null) { + // Decode the bits + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8")); - // Decode the bits - InputStreamReader inputStreamReader = new InputStreamReader(context.getResources().openRawResource(resID), Charset.forName("UTF-8")); + // Buffer the decoded characters + BufferedReader reader = new BufferedReader(inputStreamReader); - // Buffer the decoded characters - BufferedReader reader = new BufferedReader(inputStreamReader); + // Store a line of characters from the BufferedReader + String line = reader.readLine(); - // Store a line of characters from the BufferedReader - String line = reader.readLine(); - - // If not end of buffered input stream, read next line and add to output - while (line != null) { - output.append(line); - line = reader.readLine(); + // If not end of buffered input stream, read next line and add to output + while (line != null) { + output.append(line); + line = reader.readLine(); + } } // Convert chars sequence from the builder into a string and return return output.toString(); } + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private static List extractDataFromJson(String bookJSON) { + // Exit if no data was returned from the HTTP request + if (TextUtils.isEmpty(bookJSON)) + return null; - public static List extractDataFromJson(Context context) { // Initialize list of strings to hold the extracted books List books = new ArrayList<>(); + String REQUEST_URL_FULL = "https://api.itbook.store/1.0/books/"; + try { - for (int value : booksData) { - String rawJSONResponse = readFromStream(context, value); + // Create JSON object from response + JSONObject rawJSONResponse = new JSONObject(bookJSON); - // Get the current book - JSONObject currentBook = new JSONObject(rawJSONResponse); - - // Get the book's title - String title = currentBook.getString("title"); - - // Get the book's subtitle - String subtitle = currentBook.getString("subtitle"); - - // Extract authors only if they exist - String authors = ""; - if (currentBook.has("authors")) { - // Extract array "authors" - String[] authorsArray = currentBook.getString("authors").split(","); - - for (int j = 0; j < authorsArray.length; j++) { - if (j == 0) - authors += authorsArray[j]; - else if (j <= 2) - authors += " | " + authorsArray[j]; - else - authors += " and others"; - } - } else { - authors = "Unknown authors"; - } + // Extract the array that holds the books + JSONArray bookArray = rawJSONResponse.getJSONArray("books"); - // Get the book's publisher - String publisher = currentBook.getString("publisher"); + for (int i = 0; i < bookArray.length(); i++) { + // Get the current book + JSONObject currentBook = bookArray.getJSONObject(i); // Get the book's ISBN number String isbn13 = currentBook.getString("isbn13"); - // Get the book's pages - String pages = currentBook.getString("pages"); + if (!isbn13.equals("")) { + URL url = makeUrl(REQUEST_URL_FULL + isbn13); + + // Perform HTTP request to the URL and receive a JSON response back + String jsonResponse = null; + try { + jsonResponse = makeHttpRequest(url); + } catch (IOException err) { + Log.e(LOG_MSG, "Problem making the HTTP request.", err); + } + + // Get full information the current book + JSONObject currentFullBook = new JSONObject(Objects.requireNonNull(jsonResponse)); + + // Get the book's title + String title = currentFullBook.getString("title"); + + // Get the book's subtitle + String subtitle = currentFullBook.getString("subtitle"); + + // Extract authors only if they exist + String authors = ""; + if (currentFullBook.has("authors")) { + // Extract array "authors" + String[] authorsArray = currentFullBook.getString("authors").split(","); + + for (int j = 0; j < authorsArray.length; j++) { + if (j == 0) + authors += authorsArray[j]; + else if (j <= 2) + authors += " | " + authorsArray[j]; + else + authors += " and others"; + } + } else { + authors = "Unknown authors"; + } + + // Get the book's publisher + String publisher = currentFullBook.getString("publisher"); + + // Get the book's pages + String pages = currentFullBook.getString("pages"); + + // Get the book's year of publish + String year = currentFullBook.getString("year"); + + // Get the book's rating + String rating = currentFullBook.getString("rating"); + + // Get the book's description + String description = currentFullBook.getString("desc"); - // Get the book's year of publish - String year = currentBook.getString("year"); + // Get the book's price + String price = currentFullBook.getString("price"); - // Get the book's rating - String rating = currentBook.getString("rating"); + // Get the URL of book's cover + String imageUrl = currentFullBook.getString("image"); - // Get the book's description - String description = currentBook.getString("desc"); + // Create a new {@link Book} object with the title, subtitle, isbn13, price + // and imageUrl from the JSON response. + Book book = new Book(title, subtitle, authors, publisher, isbn13, pages, year, + rating, description, price, imageUrl); - // Get the book's price - String price = currentBook.getString("price"); + // Add the new {@link Book} to the list of books. + books.add(book); - // Get the URL of book's cover - String imageUrl = currentBook.getString("image"); - // Create a new {@link Book} object with the title, subtitle, isbn13, price - // and imageUrl from the JSON response. - Book book = new Book(title, subtitle, authors, publisher, isbn13, pages, year, - rating, description, price, imageUrl); + } else { + // Get the book's title + String title = currentBook.getString("title"); + + // Get the book's subtitle + String subtitle = currentBook.getString("subtitle"); + + // Get the book's price + String price = currentBook.getString("price"); + + // Get the URL of book's cover + String imageUrl = currentBook.getString("image"); - // Add the new {@link Book} to the list of books. - books.add(book); + + // Create a new {@link Book} object with the title, subtitle, isbn13, price + // and imageUrl from the JSON response. + Book book = new Book(title, subtitle, "", "", isbn13, "", "", + "0.0", "", price, imageUrl); + + // Add the new {@link Book} to the list of books. + books.add(book); + } } } catch (JSONException e) { // Catch the exception from the 'try' block and print a log message Log.e("BookJSONParser", "Problem parsing the book JSON results", e); - } catch (IOException e) { - Log.e(LOG_MSG, "Problem retrieving the book JSON results.", e); } // Return the list of books diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookLoader.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookLoader.java new file mode 100644 index 0000000..820ce79 --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/BookLoader.java @@ -0,0 +1,77 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.RequiresApi; + +import java.util.List; + + +/** Using AsyncTask to load a list of books by network request to a certain URL. */ +public class BookLoader extends AsyncTaskLoader> { + + /** Query URL **/ + private Bundle mArgs; + + /** Cache the old books */ + private List cachedBooks; + + + /** + * Constructs a new {@link BookLoader}. + * + * @param context of the activity + * @param url to load data from + */ + BookLoader(Context context, Bundle url) { + super(context); + mArgs = url; + } + + + /** Forcing the loader to make an HTTP request and start downloading the required data */ + @Override + protected void onStartLoading() { + // If args is null, return. + if (mArgs == null) { + return; + } + + // If books is not null, deliver that result. Otherwise, force a load + if (cachedBooks != null) { + deliverResult(cachedBooks); + } else { + forceLoad(); + } + } + + /** + * This method is called in a background thread and takes care of the + * generating new data from the given JSON file + */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Override + public List loadInBackground() { + // Extract the search query from the args using our constant + String searchUrl = mArgs.getString(BookActivity.REQUEST_URL); + + // Check for valid string url + if (searchUrl == null || TextUtils.isEmpty(searchUrl)) { + return null; + } + + // Perform the network request, parse the response, and extract a list of books. + return BookJSONParser.getBookData(searchUrl); + } + + /** Override deliverResult and store the data in returnedUrl */ + @Override + public void deliverResult(List data) { + cachedBooks = data; + super.deliverResult(data); + } +} diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/GalleryActivity.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/GalleryActivity.java deleted file mode 100644 index 2605538..0000000 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/GalleryActivity.java +++ /dev/null @@ -1,93 +0,0 @@ -package ua.kpi.comsys.io8227.jackshen; - -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Outline; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.provider.MediaStore; -import android.view.View; -import android.view.ViewOutlineProvider; -import android.widget.Button; -import android.widget.ImageView; - -import androidx.annotation.RequiresApi; -import androidx.appcompat.app.AppCompatActivity; - -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Objects; - - -public class GalleryActivity extends AppCompatActivity { - - /** Handler for the array of images */ - private ArrayList images = new ArrayList<>(); - - /** Handler for the number of current image */ - private int currentImage = 0; - - /** Handler for picking images */ - private int pickImage = 50; - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_gallery); - - images.addAll(Arrays.asList((ImageView) findViewById(R.id.image1), - (ImageView) findViewById(R.id.image2), - (ImageView) findViewById(R.id.image3), - (ImageView) findViewById(R.id.image4), - (ImageView) findViewById(R.id.image5), - (ImageView) findViewById(R.id.image6), - (ImageView) findViewById(R.id.image7), - (ImageView) findViewById(R.id.image8))); - - Button addImageBtn = findViewById(R.id.addImageButton); - - //Stylization button for adding images - ViewOutlineProvider viewOutlineProvider = new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - int shapeSize = getResources().getDimensionPixelSize(R.dimen.shape_size); - outline.setRoundRect(0, 0, shapeSize, shapeSize, shapeSize / 2); - } - }; - addImageBtn.setOutlineProvider(viewOutlineProvider); - addImageBtn.setClipToOutline(true); - - addImageBtn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Intent gallery = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI); - startActivityForResult(gallery, pickImage); - } - }); - } - - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent imageReturnedIntent) { - super.onActivityResult(requestCode, resultCode, imageReturnedIntent); - - if (resultCode == RESULT_OK && requestCode == pickImage) { - try { - final Uri imageUri = imageReturnedIntent.getData(); - final InputStream imageStream = getContentResolver().openInputStream(Objects.requireNonNull(imageUri)); - final Bitmap selectedImage = BitmapFactory.decodeStream(imageStream); - - images.get(currentImage).setImageBitmap(selectedImage); - currentImage = (currentImage + 1) % images.size(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - } - - } -} diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/HttpRequestImgHelper.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/HttpRequestImgHelper.java new file mode 100644 index 0000000..d8ef485 --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/HttpRequestImgHelper.java @@ -0,0 +1,92 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.app.ProgressDialog; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.view.Gravity; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +public class HttpRequestImgHelper extends AsyncTask> { + + private List mUrl; + private Context mContext; + private ProgressDialog mProgressDialog = null; + + HttpRequestImgHelper(List url, Context context) { + mUrl = url; + mContext = context; + } + + public interface OnTaskExecFinished { + void OnTaskExecFinishedEvent(List result); + } + + private static OnTaskExecFinished mTaskFinishedEvent; + + static void setOnTaskExecFinishedEvent(OnTaskExecFinished taskEvent) { + if (taskEvent != null) { + mTaskFinishedEvent = taskEvent; + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + mProgressDialog = new ProgressDialog(mContext); + mProgressDialog.setTitle("Collect images..."); + mProgressDialog.getWindow().setGravity(Gravity.CENTER); + mProgressDialog.show(); + mProgressDialog.setCancelable(false); + } + + @Override + protected List doInBackground(Void... params) { + + List images = new ArrayList<>(); + for (int i = 0; i < mUrl.size(); i++) { + Drawable result = httpRequest(mUrl.get(i)); + images.add(result); + } + + return images; + } + + @Override + protected void onPostExecute(List images) { + super.onPostExecute(images); + + if (mTaskFinishedEvent != null) { + mTaskFinishedEvent.OnTaskExecFinishedEvent(images); + } + + mProgressDialog.dismiss(); + } + + private Drawable httpRequest(String url) { + Drawable result = null; + HttpURLConnection urlConnection; + + try { + String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%"; + String encodeUrl = Uri.encode(url, ALLOWED_URI_CHARS); + URL target = new URL(encodeUrl); + urlConnection = (HttpURLConnection) target.openConnection(); + urlConnection.setRequestMethod("GET"); + urlConnection.connect(); + Bitmap imageBitmap = BitmapFactory.decodeStream(urlConnection.getInputStream()); + result = new BitmapDrawable(mContext.getResources(), imageBitmap); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } +} diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/MainActivity.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/MainActivity.java index 19b2cc1..a159a76 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/MainActivity.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/MainActivity.java @@ -90,7 +90,7 @@ public void openWebsite(View v) { } public void openGallery(View v) { - Intent intent = new Intent(this, GalleryActivity.class); + Intent intent = new Intent(this, PictureActivity.class); startActivity(intent); } diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Picture.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Picture.java new file mode 100644 index 0000000..b53562c --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Picture.java @@ -0,0 +1,17 @@ +package ua.kpi.comsys.io8227.jackshen; + +import java.io.Serializable; + +public class Picture implements Serializable { + /** URL of image */ + private String mImageUrl; + + + Picture(String imageUrl) { + this.mImageUrl = imageUrl; + } + + /** Return the URL of the image */ + String getImageUrl() { return mImageUrl; } + +} diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureActivity.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureActivity.java new file mode 100644 index 0000000..9c8fffb --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureActivity.java @@ -0,0 +1,367 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.app.LoaderManager; +import android.app.Activity; +import android.content.Context; +import android.content.Loader; +import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.textfield.TextInputEditText; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class PictureActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks> { + + /** URL for image data */ + public static final String REQUEST_URL = ""; + + /** + * Constant value for the image loader ID. + * We can choose any int, it`s really needed for multiple loaders + */ + private static final int IMAGE_LOADER_ID = 1; + + /** Adapter for the list of images */ + private PictureAdapter mAdapter; + + /** Handler for the list of images */ + List images = new ArrayList<>(); + List mImageDrawableSet = new ArrayList<>(); + + /** InputText for the image search */ + private TextInputEditText mSearchText; + + /** ImageButton for the image search */ + private ImageView mSearchButton; + + /** TextView that is displayed when the list is empty */ + private TextView mEmptyTextView; + + /** Loading indicator */ + private View mLoadingIndicator; + + private LoaderManager mLoaderManager; + + private String mQueryText; + + private SpannedGridLayoutManager mGridLayoutManager; + + private int mPageNum = 24; + + RecyclerView imageListView; + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + protected void onCreate(Bundle savedInstanceState) { + // Initialize activity on main thread. + // Bundle holds previous state when re-initialized + super.onCreate(savedInstanceState); + + // Inflate the activity's UI + setContentView(R.layout.activity_gallery); + + // Setup UI to hide soft keyboard when clicked outside the {@link EditText} + setupUI(findViewById(R.id.main_parent)); + + // Find a reference to the {@link imageListView} in the layout + imageListView = findViewById(R.id.image_recycleview); + + mGridLayoutManager = new SpannedGridLayoutManager( + new SpannedGridLayoutManager.GridSpanLookup() { + @Override + public SpannedGridLayoutManager.SpanInfo getSpanInfo(int position) { + // Conditions for 2x2 items + if (position % 8 == 1) { + return new SpannedGridLayoutManager.SpanInfo(3, 3); + } else { + return new SpannedGridLayoutManager.SpanInfo(1, 1); + } + } + }, + 4, // number of columns + 1f // how big is default item + ); + imageListView.setLayoutManager(mGridLayoutManager); + + mLoadingIndicator = findViewById(R.id.progress_bar); + mLoadingIndicator.setVisibility(View.GONE); + + if (savedInstanceState != null) { + mQueryText = savedInstanceState.getString(REQUEST_URL); + } + + // Set empty view when there is no data + mEmptyTextView = findViewById(R.id.empty_view); + if (images.isEmpty()) { + mEmptyTextView.setVisibility(View.VISIBLE); + imageListView.setVisibility(View.GONE); + } else { + imageListView.setVisibility(View.VISIBLE); + mEmptyTextView.setVisibility(View.GONE); + } + + // Create a new adapter that takes an empty list of images as input + mAdapter = new PictureAdapter(this, images, mImageDrawableSet); + + // Set the adapter on the {@link imageListView} so the list can be populated in UI + imageListView.setAdapter(mAdapter); + + + // Get a reference to the {@link mSearchButton} to implement button click via keyboard + mSearchButton = findViewById(R.id.buttonSearch); + + // Get a reference to the user input edit text view + mSearchText = findViewById(R.id.inputSearch); + + // Set the an {@link setOnClickListener} on the ImageView + // Implement search when user click on the button + mSearchButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onClickSearch(Objects.requireNonNull(mSearchText.getText()).toString()); + } + }); + + // Set the an {@link OnEditorActionListener} on the editable text view + // Implement search button click when user presses the done button on the keyboard + mSearchText.setOnEditorActionListener(new EditText.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Check whether the done button is pressed on the keyboard + if (actionId == EditorInfo.IME_ACTION_DONE) { + // User has finished entering text + // Perform the search button click programmatically + onClickSearch(Objects.requireNonNull(mSearchText.getText()).toString()); + + // Return true on successfully handling the action + return true; + } + + // Do not perform any task when user is actually entering text + // in the editable text view + return false; + } + }); + + + + /* + * Initialize the loader + */ + if (mQueryText != null) { + getLoaderManager().initLoader(IMAGE_LOADER_ID, null, this); + } + + } + + + /** This method is called when the user hits the search button */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void onClickSearch(String searchText) { + mSearchText.clearFocus(); + + images.clear(); + mImageDrawableSet.clear(); + + mEmptyTextView.setVisibility(View.GONE); + + // Get a reference to the ConnectivityManager to check state of network connectivity + ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + + // Get details on the currently active default data network + NetworkInfo networkInfo = Objects.requireNonNull(connMgr).getActiveNetworkInfo(); + + try { + // If there is a network connection -> get data + if (networkInfo != null && networkInfo.isConnected()) { + if (!searchText.isEmpty() && searchText.length() >= 3 && searchText != null) { + String imageName = URLEncoder.encode(searchText, "UTF-8"); + + // Set the URL with the suitable imageName + mQueryText = "https://pixabay.com/api/" + "?key=" + "19193969-87191e5db266905fe8936d565" + "&q=" + imageName + "&page=" + mPageNum; + + // Show the loading indicator. + mLoadingIndicator.setVisibility(View.VISIBLE); + + // Create a bundle called queryBundle + Bundle queryBundle = new Bundle(); + + // Use putString with REQUEST_URL as the key and the String value of the URL as the value + queryBundle.putString(REQUEST_URL, mQueryText); + + // Get a reference to the LoaderManager, in order to interact with loaders. + mLoaderManager = getLoaderManager(); + + // Get the loader initialized. Go through the above specified int ID constant + // and pass the bundle to null. + Loader ImagesSearchLoader = mLoaderManager.getLoader(IMAGE_LOADER_ID); + if (ImagesSearchLoader == null) { + mLoaderManager.initLoader(IMAGE_LOADER_ID, queryBundle, PictureActivity.this); + } else { + mLoaderManager.restartLoader(IMAGE_LOADER_ID, queryBundle, PictureActivity.this); + } + } else { + Toast.makeText(this, "You need to introduce some text to search (>=3 symbols)", Toast.LENGTH_LONG).show(); + } + } else { + // Otherwise, display error + // First, hide loading indicator so error message will be visible + mLoadingIndicator.setVisibility(View.INVISIBLE); + + // Update empty state with no connection error message + Toast.makeText(PictureActivity.this, "No Internet Connection", Toast.LENGTH_SHORT).show(); + mEmptyTextView.setText("No Internet Connection"); + } + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + + + /** + * Set up touch listeners on all parts of the UI besides the {@link EditText} so that the user + * can click out to hide the soft keypad + */ + public void setupUI(View view) { + // Set up touch listener for non-text box views to hide keyboard + if (!(view instanceof EditText)) { + view.setOnTouchListener(new View.OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + // Hide keypad + v.performClick(); + hideSoftKeyboard(PictureActivity.this); + return false; + } + }); + } + + //If a layout container, iterate over children and seed recursion + if (view instanceof ViewGroup) { + // Current view is a {@Link ViewGroup} + // Traverse the {@link ViewGroup}, over each child + for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) { + // Get the current child view + View innerView = ((ViewGroup) view).getChildAt(i); + // Set up touch listeners on non-text box views + setupUI(innerView); + } + } + } + + /** + * This method hides the soft keypad that pops up when there are views that solicit user input + */ + public static void hideSoftKeyboard(Activity activity) { + // Get the activity's input method service + InputMethodManager inputMethodManager = (InputMethodManager) activity.getSystemService( + Activity.INPUT_METHOD_SERVICE); + + if (inputMethodManager != null) { + if (activity.getCurrentFocus() == null) + return; + if (activity.getCurrentFocus().getWindowToken() == null) + return; + // Hide the soft keypad + inputMethodManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 0); + } + } + + @Override + public Loader> onCreateLoader(int i, Bundle args) { + return new PictureLoader(this, args); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + // Hide the indicator after the data is appeared + mLoadingIndicator.setVisibility(View.GONE); + + // Clear the adapter pf previous image data + images.clear(); + mImageDrawableSet.clear(); + + // If there is a valid list of {@link Picture}s, then add them to the adapter's + // data set. This will trigger the ListView to update + if (data != null && !data.isEmpty()) { + images.addAll(data); + + HttpRequestImgHelper requestImgHelper = new HttpRequestImgHelper(PictureJSONParser.mPreviewImageUrls, this); + requestImgHelper.setOnTaskExecFinishedEvent(new HttpRequestImgHelper.OnTaskExecFinished() { + @Override + public void OnTaskExecFinishedEvent(List result) { + + mImageDrawableSet = result; + + mAdapter = new PictureAdapter(PictureActivity.this, images, mImageDrawableSet); + imageListView.setAdapter(mAdapter); + + mEmptyTextView.setVisibility(View.GONE); + imageListView.setVisibility(View.VISIBLE); + + mGridLayoutManager = new SpannedGridLayoutManager( + new SpannedGridLayoutManager.GridSpanLookup() { + @Override + public SpannedGridLayoutManager.SpanInfo getSpanInfo(int position) { + // Conditions for 3x3 items + if (position % 8 == 1) { + return new SpannedGridLayoutManager.SpanInfo(3, 3); + } else { + return new SpannedGridLayoutManager.SpanInfo(1, 1); + } + } + }, + 4, // number of columns + 1f // how big is default item + ); + imageListView.setLayoutManager(mGridLayoutManager); + mAdapter.notifyDataSetChanged(); + } + }); + requestImgHelper.execute(); + } + + // Set empty text to display "No images found." + mEmptyTextView.setText("No images found"); + mEmptyTextView.setVisibility(View.VISIBLE); + + } + + @Override + public void onLoaderReset(Loader> loader) { + // Clear existing data on adapter as loader is reset + images.clear(); + mImageDrawableSet.clear(); + } + + /** Save the data about url */ + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(REQUEST_URL, mQueryText); + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureAdapter.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureAdapter.java new file mode 100644 index 0000000..3bfa06a --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureAdapter.java @@ -0,0 +1,95 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.InputStream; +import java.util.List; +import java.util.Objects; + +public class PictureAdapter extends RecyclerView.Adapter { + + private Context mContext; + private List mImageList; + private List mImages; + + PictureAdapter(Context context, List imageList, List images) { + mContext = context; + mImageList = imageList; + mImages = images; + } + + + @Override + public PictureViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View layoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.image_item, null); + return new PictureViewHolder(layoutView); + } + + @Override + public void onBindViewHolder(PictureViewHolder holder, final int position) { + holder.mImageView.setImageDrawable(mImages.get(position)); + if (position % 8 == 1) { + holder.mImageView.getLayoutParams().height = (int) mContext.getResources().getDimension(R.dimen.imageview_height); + holder.mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + holder.mImageView.requestLayout(); + + } +// Picture currentImage = getItem(position); +// new DownloadImage(holder.mImageView).execute(currentImage.getImageUrl()); + } + + @Override + public int getItemCount() { + return mImageList.size(); + } + + public Picture getItem(int position) { + return mImageList.get(position); + } + + + /** Class to download an image from URL */ + public static class DownloadImage extends AsyncTask { + final ImageView bmImage; + + DownloadImage(ImageView bmImage) { + this.bmImage = bmImage; + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + protected Bitmap doInBackground(String... urls) { + String urlDisplay = urls[0]; + Bitmap mIcon = null; + try { + InputStream in = new java.net.URL(urlDisplay).openStream(); + mIcon = BitmapFactory.decodeStream(in); + } catch (Exception e) { + Log.e("Error", Objects.requireNonNull(e.getMessage())); + e.printStackTrace(); + } + return mIcon; + } + + protected void onPostExecute(Bitmap result) { + bmImage.setImageBitmap(result); + } + } +} + diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureJSONParser.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureJSONParser.java new file mode 100644 index 0000000..f889e40 --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureJSONParser.java @@ -0,0 +1,202 @@ +package ua.kpi.comsys.io8227.jackshen; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** Helper methods to request and retrieve image data from a specified JSON */ +final class PictureJSONParser { + + /** Tag for the log messages */ + private static final String LOG_MSG = PictureJSONParser.class.getSimpleName(); + + static List mPreviewImageUrls = new ArrayList<>(); + + /** + * We are creating a private constructor because no one else should create + * the {@link PictureJSONParser} object. + */ + private PictureJSONParser() { + } + + /** + * Query given JSON and return a list of {@link String} objects. + * + * @param requestUrl - our URL as a {@link String} object + * @return the list of {@link Picture}s + */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + static List getPictureData(String requestUrl) { + // Create URL object + URL url = makeUrl(requestUrl); + + // Perform HTTP request to the URL and receive a JSON response back + String jsonResponse = null; + try { + jsonResponse = makeHttpRequest(url); + } catch (IOException err) { + Log.e(LOG_MSG, "Problem making the HTTP request.", err); + } + + // Extract relevant fields from the JSON response and create a list of {@link Picture}s + // Then return the list of {@link Picture}s + return extractDataFromJson(jsonResponse); + } + + /** Returns new URL object from the given string URL. */ + private static URL makeUrl(String strUrl) { + // Initialize an empty {@link URL} object to hold the parsed URL from the strUrl + URL url = null; + + // Parse valid URL from param strUrl and handle Malformed urls + try { + url = new URL(strUrl); + } catch (MalformedURLException err) { + Log.e(LOG_MSG, "Problem building the URL!", err); + } + + // Return valid URL + return url; + } + + /** Make an HTTP request to the given URL and return a String as the response. */ + private static String makeHttpRequest(URL url) throws IOException { + // Initialize variable to hold the parsed JSON response + String jsonResponse = ""; + + // Return response if URL is null + if (url == null) { + return jsonResponse; + } + + // Initialize HTTP connection object + HttpURLConnection connection = null; + + // Initialize {@link InputStream} to hold response from request + InputStream inputStream = null; + try { + // Establish connection to the URL + connection = (HttpURLConnection) url.openConnection(); + + // Set request type + connection.setRequestMethod("GET"); + + // Setting how long to wait on the request (in milliseconds) + connection.setReadTimeout(10000); + connection.setConnectTimeout(15000); + + // Establish connection to the URL + connection.connect(); + + // Check for successful connection + if (connection.getResponseCode() == 200) { + inputStream = connection.getInputStream(); + jsonResponse = readFromStream(inputStream); + } else { + Log.e(LOG_MSG, "Error response code: " + connection.getResponseCode()); + } + } catch (IOException err) { + Log.e(LOG_MSG, "Problem retrieving the image JSON results.", err); + } finally { + if (connection != null) { + // Disconnect the connection after successfully making the HTTP request + connection.disconnect(); + } + + if (inputStream != null) { + // Close the stream after successfully parsing the request + // This also can throw an IOException + inputStream.close(); + } + } + + // Return JSON as a {@link String} + return jsonResponse; + } + + /** + * Convert the {@link InputStream} into a String which contains the whole JSON response. + */ + private static String readFromStream(InputStream inputStream) throws IOException { + StringBuilder output = new StringBuilder(); + if (inputStream != null) { + // Decode the bits + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8")); + + // Buffer the decoded characters + BufferedReader reader = new BufferedReader(inputStreamReader); + + // Store a line of characters from the BufferedReader + String line = reader.readLine(); + + // If not end of buffered input stream, read next line and add to output + while (line != null) { + output.append(line); + line = reader.readLine(); + } + } + + // Convert chars sequence from the builder into a string and return + return output.toString(); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private static List extractDataFromJson(String imageJSON) { + // Exit if no data was returned from the HTTP request + if (TextUtils.isEmpty(imageJSON)) + return null; + + // Initialize list of strings to hold the extracted images + List images = new ArrayList<>(); + + try { + // Create JSON object from response + JSONObject rawJSONResponse = new JSONObject(imageJSON); + + // Extract the array that holds the images + JSONArray imageArray = rawJSONResponse.getJSONArray("hits"); + + for (int i = 0; i < imageArray.length(); i++) { + // Get the current image + JSONObject currentImage = imageArray.getJSONObject(i); + + // Get the URL of image's cover + String imageUrl = currentImage.getString("webformatURL"); + + mPreviewImageUrls.add(imageUrl); + + // Create a new {@link Picture} object with the title, subtitle, isbn13, price + // and imageUrl from the JSON response. + Picture image = new Picture(imageUrl); + + // Add the new {@link Picture} to the list of images. + images.add(image); + } + + } catch (JSONException e) { + // Catch the exception from the 'try' block and print a log message + Log.e("ImageJSONParser", "Problem parsing the image JSON results", e); + } + + // Return the list of images + return images; + } + +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureLoader.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureLoader.java new file mode 100644 index 0000000..2ff6568 --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureLoader.java @@ -0,0 +1,77 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.RequiresApi; + +import java.util.List; + + +/** Using AsyncTask to load a list of images by network request to a certain URL. */ +public class PictureLoader extends AsyncTaskLoader> { + + /** Query URL **/ + private Bundle mArgs; + + /** Cache the old images */ + private List cachedImages; + + + /** + * Constructs a new {@link PictureLoader}. + * + * @param context of the activity + * @param url to load data from + */ + PictureLoader(Context context, Bundle url) { + super(context); + mArgs = url; + } + + + /** Forcing the loader to make an HTTP request and start downloading the required data */ + @Override + protected void onStartLoading() { + // If args is null, return. + if (mArgs == null) { + return; + } + + // If images is not null, deliver that result. Otherwise, force a load + if (cachedImages != null) { + deliverResult(cachedImages); + } else { + forceLoad(); + } + } + + /** + * This method is called in a background thread and takes care of the + * generating new data from the given JSON file + */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Override + public List loadInBackground() { + // Extract the search query from the args using our constant + String searchUrl = mArgs.getString(PictureActivity.REQUEST_URL); + + // Check for valid string url + if (searchUrl == null || TextUtils.isEmpty(searchUrl)) { + return null; + } + + // Perform the network request, parse the response, and extract a list of images. + return PictureJSONParser.getPictureData(searchUrl); + } + + /** Override deliverResult and store the data in returnedUrl */ + @Override + public void deliverResult(List data) { + cachedImages = data; + super.deliverResult(data); + } +} diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureViewHolder.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureViewHolder.java new file mode 100644 index 0000000..930b0ec --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/PictureViewHolder.java @@ -0,0 +1,17 @@ +package ua.kpi.comsys.io8227.jackshen; + +import androidx.recyclerview.widget.RecyclerView; +import android.view.View; +import android.widget.ImageView; + + +class PictureViewHolder extends RecyclerView.ViewHolder { + + ImageView mImageView; + + PictureViewHolder(View itemView) { + super(itemView); + + mImageView = itemView.findViewById(R.id.image_result); + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/SpannedGridLayoutManager.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/SpannedGridLayoutManager.java new file mode 100644 index 0000000..66547c4 --- /dev/null +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/SpannedGridLayoutManager.java @@ -0,0 +1,630 @@ +package ua.kpi.comsys.io8227.jackshen; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.PointF; +import android.graphics.Rect; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link RecyclerView.LayoutManager} which displays a regular grid (i.e. all cells are the same + * size) and allows simultaneous row & column spanning. + */ +public class SpannedGridLayoutManager extends RecyclerView.LayoutManager { + + private GridSpanLookup spanLookup; + private int columns = 1; + private float cellAspectRatio = 1f; + + private int cellHeight; + private int[] cellBorders; + private int firstVisiblePosition; + private int lastVisiblePosition; + private int firstVisibleRow; + private int lastVisibleRow; + private boolean forceClearOffsets; + private SparseArray cells; + private List firstChildPositionForRow; // key == row, val == first child position + private int totalRows; + private final Rect itemDecorationInsets = new Rect(); + + public SpannedGridLayoutManager(GridSpanLookup spanLookup, int columns, float cellAspectRatio) { + this.spanLookup = spanLookup; + this.columns = columns; + this.cellAspectRatio = cellAspectRatio; + setAutoMeasureEnabled(true); + } + + @Keep /* XML constructor, see RecyclerView#createLayoutManager */ + public SpannedGridLayoutManager( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.SpannedGridLayoutManager, defStyleAttr, defStyleRes); + columns = a.getInt(R.styleable.SpannedGridLayoutManager_spanCount, 1); + parseAspectRatio(a.getString(R.styleable.SpannedGridLayoutManager_aspectRatio)); + // TODO use this! + int orientation = a.getInt( + R.styleable.SpannedGridLayoutManager_android_orientation, RecyclerView.VERTICAL); + a.recycle(); + setAutoMeasureEnabled(true); + } + + public interface GridSpanLookup { + SpanInfo getSpanInfo(int position); + } + + public void setSpanLookup(@NonNull GridSpanLookup spanLookup) { + this.spanLookup = spanLookup; + } + + public static class SpanInfo { + public int columnSpan; + public int rowSpan; + + public SpanInfo(int columnSpan, int rowSpan) { + this.columnSpan = columnSpan; + this.rowSpan = rowSpan; + } + + public static final SpanInfo SINGLE_CELL = new SpanInfo(1, 1); + } + + public static class LayoutParams extends RecyclerView.LayoutParams { + + int columnSpan; + int rowSpan; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(RecyclerView.LayoutParams source) { + super(source); + } + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + calculateWindowSize(); + calculateCellPositions(recycler, state); + + if (state.getItemCount() == 0) { + detachAndScrapAttachedViews(recycler); + firstVisibleRow = 0; + resetVisibleItemTracking(); + return; + } + + // TODO use orientationHelper + int startTop = getPaddingTop(); + int scrollOffset = 0; + if (forceClearOffsets) { // see #scrollToPosition + startTop = -(firstVisibleRow * cellHeight); + forceClearOffsets = false; + } else if (getChildCount() != 0) { + scrollOffset = getDecoratedTop(getChildAt(0)); + startTop = scrollOffset - (firstVisibleRow * cellHeight); + resetVisibleItemTracking(); + } + + detachAndScrapAttachedViews(recycler); + int row = firstVisibleRow; + int availableSpace = getHeight() - scrollOffset; + int lastItemPosition = state.getItemCount() - 1; + while (availableSpace > 0 && lastVisiblePosition < lastItemPosition) { + availableSpace -= layoutRow(row, startTop, recycler, state); + row = getNextSpannedRow(row); + } + + layoutDisappearingViews(recycler, state, startTop); + } + + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + @Override + public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { + return new LayoutParams(c, attrs); + } + + @Override + public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + if (lp instanceof ViewGroup.MarginLayoutParams) { + return new LayoutParams((ViewGroup.MarginLayoutParams) lp); + } else { + return new LayoutParams(lp); + } + } + + @Override + public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { + return lp instanceof LayoutParams; + } + + @Override + public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { + removeAllViews(); + reset(); + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return true; + } + + @Override + public boolean canScrollVertically() { + return true; + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state){ + if (getChildCount() == 0 || dy == 0) return 0; + + int scrolled; + int top = getDecoratedTop(getChildAt(0)); + + if (dy < 0) { // scrolling content down + if (firstVisibleRow == 0) { // at top of content + int scrollRange = -(getPaddingTop() - top); + scrolled = Math.max(dy, scrollRange); + } else { + scrolled = dy; + } + if (top - scrolled >= 0) { // new top row came on screen + int newRow = firstVisibleRow - 1; + if (newRow >= 0) { + int startOffset = top - (firstVisibleRow * cellHeight); + layoutRow(newRow, startOffset, recycler, state); + } + } + int firstPositionOfLastRow = getFirstPositionInSpannedRow(lastVisibleRow); + int lastRowTop = getDecoratedTop( + getChildAt(firstPositionOfLastRow - firstVisiblePosition)); + if (lastRowTop - scrolled > getHeight()) { // last spanned row scrolled out + recycleRow(lastVisibleRow, recycler, state); + } + } else { // scrolling content up + int bottom = getDecoratedBottom(getChildAt(getChildCount() - 1)); + if (lastVisiblePosition == getItemCount() - 1) { // is at end of content + int scrollRange = Math.max(bottom - getHeight() + getPaddingBottom(), 0); + scrolled = Math.min(dy, scrollRange); + } else { + scrolled = dy; + } + if ((bottom - scrolled) < getHeight()) { // new row scrolled in + int nextRow = lastVisibleRow + 1; + if (nextRow < getSpannedRowCount()) { + int startOffset = top - (firstVisibleRow * cellHeight); + layoutRow(nextRow, startOffset, recycler, state); + } + } + int lastPositionInRow = getLastPositionInSpannedRow(firstVisibleRow, state); + int bottomOfFirstRow = + getDecoratedBottom(getChildAt(lastPositionInRow - firstVisiblePosition)); + if (bottomOfFirstRow - scrolled < 0) { // first spanned row scrolled out + recycleRow(firstVisibleRow, recycler, state); + } + } + offsetChildrenVertical(-scrolled); + return scrolled; + } + + @Override + public void scrollToPosition(int position) { + if (position >= getItemCount()) position = getItemCount() - 1; + + firstVisibleRow = getRowIndex(position); + resetVisibleItemTracking(); + forceClearOffsets = true; + removeAllViews(); + requestLayout(); + } + + @Override + public void smoothScrollToPosition( + RecyclerView recyclerView, RecyclerView.State state, int position) { + if (position >= getItemCount()) position = getItemCount() - 1; + + LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) { + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + final int rowOffset = getRowIndex(targetPosition) - firstVisibleRow; + return new PointF(0, rowOffset * cellHeight); + } + }; + scroller.setTargetPosition(position); + startSmoothScroll(scroller); + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + // TODO update this to incrementally calculate + return getSpannedRowCount() * cellHeight + getPaddingTop() + getPaddingBottom(); + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + return getHeight(); + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + if (getChildCount() == 0) return 0; + return getPaddingTop() + (firstVisibleRow * cellHeight) - getDecoratedTop(getChildAt(0)); + } + + @Override + public View findViewByPosition(int position) { + if (position < firstVisiblePosition || position > lastVisiblePosition) return null; + return getChildAt(position - firstVisiblePosition); + } + + public int getFirstVisibleItemPosition() { + return firstVisiblePosition; + } + + private static class GridCell { + final int row; + final int rowSpan; + final int column; + final int columnSpan; + + GridCell(int row, int rowSpan, int column, int columnSpan) { + this.row = row; + this.rowSpan = rowSpan; + this.column = column; + this.columnSpan = columnSpan; + } + } + + /** + * This is the main layout algorithm, iterates over all items and places them into [column, row] + * cell positions. Stores this layout info for use later on. Also records the adapter position + * that each row starts at. + *

+ * Note that if a row is spanned, then the row start position is recorded as the first cell of + * the row that the spanned cell starts in. This is to ensure that we have sufficient contiguous + * views to layout/draw a spanned row. + */ + private void calculateCellPositions(RecyclerView.Recycler recycler, RecyclerView.State state) { + final int itemCount = state.getItemCount(); + cells = new SparseArray<>(itemCount); + firstChildPositionForRow = new ArrayList<>(); + int row = 0; + int column = 0; + recordSpannedRowStartPosition(row, column); + int[] rowHWM = new int[columns]; // row high water mark (per column) + for (int position = 0; position < itemCount; position++) { + + SpanInfo spanInfo; + int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(position); + if (adapterPosition != RecyclerView.NO_POSITION) { + spanInfo = spanLookup.getSpanInfo(adapterPosition); + } else { + // item removed from adapter, retrieve its previous span info + // as we can't get from the lookup (adapter) + spanInfo = getSpanInfoFromAttachedView(position); + } + + if (spanInfo.columnSpan > columns) { + spanInfo.columnSpan = columns; // or should we throw? + } + + // check horizontal space at current position else start a new row + // note that this may leave gaps in the grid; we don't backtrack to try and fit + // subsequent cells into gaps. We place the responsibility on the adapter to provide + // continuous data i.e. that would not span column boundaries to avoid gaps. + if (column + spanInfo.columnSpan > columns) { + row++; + recordSpannedRowStartPosition(row, position); + column = 0; + } + + // check if this cell is already filled (by previous spanning cell) + while (rowHWM[column] > row) { + column++; + if (column + spanInfo.columnSpan > columns) { + row++; + recordSpannedRowStartPosition(row, position); + column = 0; + } + } + + // by this point, cell should fit at [column, row] + cells.put(position, new GridCell(row, spanInfo.rowSpan, column, spanInfo.columnSpan)); + + // update the high water mark book-keeping + for (int columnsSpanned = 0; columnsSpanned < spanInfo.columnSpan; columnsSpanned++) { + rowHWM[column + columnsSpanned] = row + spanInfo.rowSpan; + } + + // if we're spanning rows then record the 'first child position' as the first item + // *in the row the spanned item starts*. i.e. the position might not actually sit + // within the row but it is the earliest position we need to render in order to fill + // the requested row. + if (spanInfo.rowSpan > 1) { + int rowStartPosition = getFirstPositionInSpannedRow(row); + for (int rowsSpanned = 1; rowsSpanned < spanInfo.rowSpan; rowsSpanned++) { + int spannedRow = row + rowsSpanned; + recordSpannedRowStartPosition(spannedRow, rowStartPosition); + } + } + + // increment the current position + column += spanInfo.columnSpan; + } + totalRows = rowHWM[0]; + for (int i = 1; i < rowHWM.length; i++) { + if (rowHWM[i] > totalRows) { + totalRows = rowHWM[i]; + } + } + } + + private SpanInfo getSpanInfoFromAttachedView(int position) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (position == getPosition(child)) { + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + return new SpanInfo(lp.columnSpan, lp.rowSpan); + } + } + // errrrr? + return SpanInfo.SINGLE_CELL; + } + + private void recordSpannedRowStartPosition(final int rowIndex, final int position) { + if (getSpannedRowCount() < (rowIndex + 1)) { + firstChildPositionForRow.add(position); + } + } + + private int getRowIndex(final int position) { + return position < cells.size() ? cells.get(position).row : -1; + } + + private int getSpannedRowCount() { + return firstChildPositionForRow.size(); + } + + private int getNextSpannedRow(int rowIndex) { + int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex); + int nextRow = rowIndex + 1; + while (nextRow < getSpannedRowCount() + && getFirstPositionInSpannedRow(nextRow) == firstPositionInRow) { + nextRow++; + } + return nextRow; + } + + private int getFirstPositionInSpannedRow(int rowIndex) { + return firstChildPositionForRow.get(rowIndex); + } + + private int getLastPositionInSpannedRow(final int rowIndex, RecyclerView.State state) { + int nextRow = getNextSpannedRow(rowIndex); + return (nextRow != getSpannedRowCount()) ? // check if reached boundary + getFirstPositionInSpannedRow(nextRow) - 1 + : state.getItemCount() - 1; + } + + /** + * Lay out a given 'row'. We might actually add more that one row if the requested row contains + * a row-spanning cell. Returns the pixel height of the rows laid out. + *

+ * To simplify logic & book-keeping, views are attached in adapter order, that is child 0 will + * always be the earliest position displayed etc. + */ + private int layoutRow( + int rowIndex, int startTop, RecyclerView.Recycler recycler, RecyclerView.State state) { + int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex); + int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state); + boolean containsRemovedItems = false; + + int insertPosition = (rowIndex < firstVisibleRow) ? 0 : getChildCount(); + for (int position = firstPositionInRow; + position <= lastPositionInRow; + position++, insertPosition++) { + + View view = recycler.getViewForPosition(position); + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + containsRemovedItems |= lp.isItemRemoved(); + GridCell cell = cells.get(position); + addView(view, insertPosition); + + // TODO use orientation helper + int wSpec = getChildMeasureSpec( + cellBorders[cell.column + cell.columnSpan] - cellBorders[cell.column], + View.MeasureSpec.EXACTLY, 0, lp.width, false); + int hSpec = getChildMeasureSpec(cell.rowSpan * cellHeight, + View.MeasureSpec.EXACTLY, 0, lp.height, true); + measureChildWithDecorationsAndMargin(view, wSpec, hSpec); + + int left = cellBorders[cell.column] + lp.leftMargin; + int top = startTop + (cell.row * cellHeight) + lp.topMargin; + int right = left + getDecoratedMeasuredWidth(view); + int bottom = top + getDecoratedMeasuredHeight(view); + layoutDecorated(view, left, top, right, bottom); + lp.columnSpan = cell.columnSpan; + lp.rowSpan = cell.rowSpan; + } + + if (firstPositionInRow < firstVisiblePosition) { + firstVisiblePosition = firstPositionInRow; + firstVisibleRow = getRowIndex(firstVisiblePosition); + } + if (lastPositionInRow > lastVisiblePosition) { + lastVisiblePosition = lastPositionInRow; + lastVisibleRow = getRowIndex(lastVisiblePosition); + } + if (containsRemovedItems) return 0; // don't consume space for rows with disappearing items + + GridCell first = cells.get(firstPositionInRow); + GridCell last = cells.get(lastPositionInRow); + return (last.row + last.rowSpan - first.row) * cellHeight; + } + + /** + * Remove and recycle all items in this 'row'. If the row includes a row-spanning cell then all + * cells in the spanned rows will be removed. + */ + private void recycleRow( + int rowIndex, RecyclerView.Recycler recycler, RecyclerView.State state) { + int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex); + int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state); + int toRemove = lastPositionInRow; + while (toRemove >= firstPositionInRow) { + int index = toRemove - firstVisiblePosition; + removeAndRecycleViewAt(index, recycler); + toRemove--; + } + if (rowIndex == firstVisibleRow) { + firstVisiblePosition = lastPositionInRow + 1; + firstVisibleRow = getRowIndex(firstVisiblePosition); + } + if (rowIndex == lastVisibleRow) { + lastVisiblePosition = firstPositionInRow - 1; + lastVisibleRow = getRowIndex(lastVisiblePosition); + } + } + + private void layoutDisappearingViews( + RecyclerView.Recycler recycler, RecyclerView.State state, int startTop) { + // TODO + } + + private void calculateWindowSize() { + // TODO use OrientationHelper#getTotalSpace + int cellWidth = + (int) Math.floor((getWidth() - getPaddingLeft() - getPaddingRight()) / columns); + cellHeight = (int) Math.floor(cellWidth * (1f / cellAspectRatio)); + calculateCellBorders(); + } + + private void reset() { + cells = null; + firstChildPositionForRow = null; + firstVisiblePosition = 0; + firstVisibleRow = 0; + lastVisiblePosition = 0; + lastVisibleRow = 0; + cellHeight = 0; + forceClearOffsets = false; + } + + private void resetVisibleItemTracking() { + // maintain the firstVisibleRow but reset other state vars + // TODO make orientation agnostic + int minimumVisibleRow = getMinimumFirstVisibleRow(); + if (firstVisibleRow > minimumVisibleRow) firstVisibleRow = minimumVisibleRow; + firstVisiblePosition = getFirstPositionInSpannedRow(firstVisibleRow); + lastVisibleRow = firstVisibleRow; + lastVisiblePosition = firstVisiblePosition; + } + + private int getMinimumFirstVisibleRow() { + int maxDisplayedRows = (int) Math.ceil((float) getHeight() / cellHeight) + 1; + if (totalRows < maxDisplayedRows) return 0; + int minFirstRow = totalRows - maxDisplayedRows; + // adjust to spanned rows + return getRowIndex(getFirstPositionInSpannedRow(minFirstRow)); + } + + /* Adapted from GridLayoutManager */ + + private void calculateCellBorders() { + cellBorders = new int[columns + 1]; + int totalSpace = getWidth() - getPaddingLeft() - getPaddingRight(); + int consumedPixels = getPaddingLeft(); + cellBorders[0] = consumedPixels; + int sizePerSpan = totalSpace / columns; + int sizePerSpanRemainder = totalSpace % columns; + int additionalSize = 0; + for (int i = 1; i <= columns; i++) { + int itemSize = sizePerSpan; + additionalSize += sizePerSpanRemainder; + if (additionalSize > 0 && (columns - additionalSize) < sizePerSpanRemainder) { + itemSize += 1; + additionalSize -= columns; + } + consumedPixels += itemSize; + cellBorders[i] = consumedPixels; + } + } + + private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) { + calculateItemDecorationsForChild(child, itemDecorationInsets); + RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); + widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + itemDecorationInsets.left, + lp.rightMargin + itemDecorationInsets.right); + heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + itemDecorationInsets.top, + lp.bottomMargin + itemDecorationInsets.bottom); + child.measure(widthSpec, heightSpec); + } + + private int updateSpecWithExtra(int spec, int startInset, int endInset) { + if (startInset == 0 && endInset == 0) { + return spec; + } + int mode = View.MeasureSpec.getMode(spec); + if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { + return View.MeasureSpec.makeMeasureSpec( + View.MeasureSpec.getSize(spec) - startInset - endInset, mode); + } + return spec; + } + + /* Adapted from ConstraintLayout */ + + private void parseAspectRatio(String aspect) { + if (aspect != null) { + int colonIndex = aspect.indexOf(':'); + if (colonIndex >= 0 && colonIndex < aspect.length() - 1) { + String nominator = aspect.substring(0, colonIndex); + String denominator = aspect.substring(colonIndex + 1); + if (nominator.length() > 0 && denominator.length() > 0) { + try { + float nominatorValue = Float.parseFloat(nominator); + float denominatorValue = Float.parseFloat(denominator); + if (nominatorValue > 0 && denominatorValue > 0) { + cellAspectRatio = Math.abs(nominatorValue / denominatorValue); + return; + } + } catch (NumberFormatException e) { + // Ignore + } + } + } + } + throw new IllegalArgumentException("Could not parse aspect ratio: '" + aspect + "'"); + } + +} \ No newline at end of file diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/CoordinateJS.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/CoordinateJS.java similarity index 92% rename from app/src/main/java/ua/kpi/comsys/io8227/jackshen/CoordinateJS.java rename to app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/CoordinateJS.java index 9f557ed..fbc6deb 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/CoordinateJS.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/CoordinateJS.java @@ -1,6 +1,6 @@ -package ua.kpi.comsys.io8227.jackshen; +package ua.kpi.comsys.io8227.jackshen.coordinateJS; -import ua.kpi.comsys.io8227.jackshen.exception.GeoCoordException; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.exception.GeoCoordException; /** * This class is a little wrapper of Latitude and Longitude. @@ -33,7 +33,7 @@ public CoordinateJS(final Latitude lat, final Longitude lng) { * * @throws GeoCoordException if any parameter is null */ - CoordinateJS(final Latitude lat, final Longitude lng, final String name) { + public CoordinateJS(final Latitude lat, final Longitude lng, final String name) { this(lat, lng); setName(name); } @@ -62,7 +62,7 @@ private void setLongitude(final Longitude lng) { this.lng = lng; } - String getName() { + public String getName() { return name; } diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Latitude.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/Latitude.java similarity index 92% rename from app/src/main/java/ua/kpi/comsys/io8227/jackshen/Latitude.java rename to app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/Latitude.java index a80e325..195736a 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Latitude.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/Latitude.java @@ -1,8 +1,8 @@ -package ua.kpi.comsys.io8227.jackshen; +package ua.kpi.comsys.io8227.jackshen.coordinateJS; -import ua.kpi.comsys.io8227.jackshen.exception.GeoCoordException; -import ua.kpi.comsys.io8227.jackshen.coordinate.AbsGeoCoordinate; -import ua.kpi.comsys.io8227.jackshen.coordinate.LatLngDirection; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.exception.GeoCoordException; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.coordinate.AbsGeoCoordinate; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.coordinate.LatLngDirection; /** * Latitude indicates whether a location is North or South of the Equator (located at latitude 0). diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Longitude.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/Longitude.java similarity index 92% rename from app/src/main/java/ua/kpi/comsys/io8227/jackshen/Longitude.java rename to app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/Longitude.java index bbd1b24..071d428 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/Longitude.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/Longitude.java @@ -1,8 +1,8 @@ -package ua.kpi.comsys.io8227.jackshen; +package ua.kpi.comsys.io8227.jackshen.coordinateJS; -import ua.kpi.comsys.io8227.jackshen.exception.GeoCoordException; -import ua.kpi.comsys.io8227.jackshen.coordinate.AbsGeoCoordinate; -import ua.kpi.comsys.io8227.jackshen.coordinate.LatLngDirection; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.exception.GeoCoordException; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.coordinate.AbsGeoCoordinate; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.coordinate.LatLngDirection; /** * Longitude indicates whether a location is East or West of the prime meridian (located at longitude 0). diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/calculator/DistanceCalculator.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/calculator/DistanceCalculator.java similarity index 88% rename from app/src/main/java/ua/kpi/comsys/io8227/jackshen/calculator/DistanceCalculator.java rename to app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/calculator/DistanceCalculator.java index 1e50462..61d372d 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/calculator/DistanceCalculator.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/calculator/DistanceCalculator.java @@ -1,9 +1,9 @@ -package ua.kpi.comsys.io8227.jackshen.calculator; +package ua.kpi.comsys.io8227.jackshen.coordinateJS.calculator; -import ua.kpi.comsys.io8227.jackshen.Latitude; -import ua.kpi.comsys.io8227.jackshen.Longitude; -import ua.kpi.comsys.io8227.jackshen.CoordinateJS; -import ua.kpi.comsys.io8227.jackshen.exception.GeoCoordException; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.Latitude; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.Longitude; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.CoordinateJS; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.exception.GeoCoordException; /** * This class calculates the distance between the coordinates using the Haversine formula, which I @@ -13,7 +13,7 @@ * * @see Haversine formula */ -class DistanceCalculator { +public class DistanceCalculator { public enum Unit { @@ -45,7 +45,7 @@ public enum Unit { * * @return The total distance traveled, expressed in terms of unit */ - static double distance(final Unit unit, final CoordinateJS ... coordinates) { + public static double distance(final Unit unit, final CoordinateJS... coordinates) { if (unit == null) { throw new GeoCoordException("Unit is null"); } @@ -100,7 +100,7 @@ static double distance(final Unit unit, final CoordinateJS ... coordinates) { * * @return new CoordinateJS object, string representation will be in the form like {xx°yy'zz"Z , xx°yy'zz"Z} */ - static CoordinateJS avarageCoord(final CoordinateJS point1, final CoordinateJS point2) { + public static CoordinateJS avarageCoord(final CoordinateJS point1, final CoordinateJS point2) { if (point1 == null || point2 == null) throw new GeoCoordException("Coordinates are null"); diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/AbsGeoCoordinate.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/AbsGeoCoordinate.java similarity index 96% rename from app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/AbsGeoCoordinate.java rename to app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/AbsGeoCoordinate.java index 6253ad6..9b3b6ed 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/AbsGeoCoordinate.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/AbsGeoCoordinate.java @@ -1,12 +1,12 @@ -package ua.kpi.comsys.io8227.jackshen.coordinate; +package ua.kpi.comsys.io8227.jackshen.coordinateJS.coordinate; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; -import ua.kpi.comsys.io8227.jackshen.Latitude; -import ua.kpi.comsys.io8227.jackshen.Longitude; -import ua.kpi.comsys.io8227.jackshen.exception.GeoCoordException; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.Latitude; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.Longitude; +import ua.kpi.comsys.io8227.jackshen.coordinateJS.exception.GeoCoordException; /** * This class is used as an internal implementation logic between Latitude and Longitude. diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/GeoCoordinate.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/GeoCoordinate.java similarity index 74% rename from app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/GeoCoordinate.java rename to app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/GeoCoordinate.java index f22f8c8..8f0663b 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/GeoCoordinate.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/GeoCoordinate.java @@ -1,4 +1,4 @@ -package ua.kpi.comsys.io8227.jackshen.coordinate; +package ua.kpi.comsys.io8227.jackshen.coordinateJS.coordinate; public interface GeoCoordinate { int getDegrees(); diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/LatLngDirection.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/LatLngDirection.java similarity index 50% rename from app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/LatLngDirection.java rename to app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/LatLngDirection.java index 1bf300d..b518fd4 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinate/LatLngDirection.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/coordinate/LatLngDirection.java @@ -1,4 +1,4 @@ -package ua.kpi.comsys.io8227.jackshen.coordinate; +package ua.kpi.comsys.io8227.jackshen.coordinateJS.coordinate; public interface LatLngDirection { String getAbbreviation(); } diff --git a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/exception/GeoCoordException.java b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/exception/GeoCoordException.java similarity index 96% rename from app/src/main/java/ua/kpi/comsys/io8227/jackshen/exception/GeoCoordException.java rename to app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/exception/GeoCoordException.java index 10dd4f9..7586cac 100644 --- a/app/src/main/java/ua/kpi/comsys/io8227/jackshen/exception/GeoCoordException.java +++ b/app/src/main/java/ua/kpi/comsys/io8227/jackshen/coordinateJS/exception/GeoCoordException.java @@ -1,4 +1,4 @@ -package ua.kpi.comsys.io8227.jackshen.exception; +package ua.kpi.comsys.io8227.jackshen.coordinateJS.exception; public class GeoCoordException extends RuntimeException { diff --git a/app/src/main/res/drawable/grid.png b/app/src/main/res/drawable/grid.png new file mode 100644 index 0000000000000000000000000000000000000000..2f2f3d48a170b3d1237d9c5eb402aa7728fab98d GIT binary patch literal 493 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVBF&A;uunK>+NhqZ>L0wR(Vw^ z8KpV5V&+ImNvmY^{^I02`;|GT=h#1ov%G&9+83E>Omu5sCbr!{AhSSV?KJ5l&#T{F z_%)YV{7EbGz2nvV?*sK6ge%y@4uq>6Uu5w-ZE3|tCI7Elti4&w80@r3c$AF>z!KAbU%@S_aC*R0c z2=4Jsu%5Y>DZi;){DxPWe^RSG$Eq!_Hy9sa;DMsq5Bjqn1zpm(`9b9UJ`3&JO2J#3 z*G`#KTrB%dH-Uj|gAvQ}1FaHG%N>{Q6?Fb!F!L^Rf75r16_>&f1bmq1?)bv$4bW1k z%T5FZN}qYvE_gjLy@7ED-||@u?oH+FJNOR0@cFQjSu!J|7wWSct{;F8eJOk!7)P{Q!%#jlG3-{tJSQh!~4S z-E9`aQvxcAnk{yO*~fdyBbg$1<{lVg=EaaX@6Nn)?%V-|LJ=}LtiSt#TEC5dp zwDW||1YBp(0XshHfPoC38>piKznRyZfbmAa8nETU3v>a$1bTs2V9J#pE((nL95Ca8 zfGglZ=+r-RWryd1KZ3&l30RNofLoI#sfS}=vQ9|#30VZI#^C*jRrs6Nt)9wuV$P5I2XiAUGDHMvJ@fT<5ZcR867=Hi& N002ovPDHLkV1mN87RUeq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/rect_background.xml b/app/src/main/res/drawable/rect_background.xml new file mode 100644 index 0000000..fe8ea75 --- /dev/null +++ b/app/src/main/res/drawable/rect_background.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book.xml b/app/src/main/res/layout/activity_book.xml index 2cc7a6e..aa9f83e 100644 --- a/app/src/main/res/layout/activity_book.xml +++ b/app/src/main/res/layout/activity_book.xml @@ -76,24 +76,6 @@ android:layout_height="match_parent" android:layout_below="@id/separator"> -