…
Preparing the API Client
We will be using Android support library in this project. Add dependencies in app level build.gradle
implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support:design:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'com.squareup.retrofit2:retrofit:2.5.0' implementation 'com.squareup.retrofit2:converter-gson:2.5.0' implementation 'com.squareup.picasso:picasso:2.71828'
Go and join Unsplash, create an app and note your Access Key.
In your Android Studio project, create a Config class and store the keys.
Config.java
public class Config { public static final String unsplash_access_key = "YOUR_ACCESS_KEY_FROM_UNSPLASH"; public static final String BASE_URL_UNSPLASH = "https://api.unsplash.com/"; }
Create the retrofit Client and Interface classes.
UnsplashClient.java
import okhttp3.OkHttpClient; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class UnsplashClient { private static Retrofit retrofit = null; public static Retrofit getUnsplashClient() { if (retrofit == null) { OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new HeaderInterceptor(Config.unsplash_access_key)).build(); retrofit = new Retrofit.Builder() .baseUrl(Config.BASE_URL_UNSPLASH) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; } }
HeaderInterceptor.java
import java.io.IOException; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; public class HeaderInterceptor implements Interceptor { private String clientId; public HeaderInterceptor(String clientId) { this.clientId = clientId; } @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); request = request.newBuilder() .addHeader("Authorization", "Client-ID " + clientId) .build(); return chain.proceed(request); } }
UnsplashInterface.java
import java.util.List; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Path; import retrofit2.http.Query; public interface UnsplashInterface { @GET("photos/{id}") Call<Photo> getPhoto(@Path("id") String id, @Query("w") Integer width, @Query("h") Integer height); @GET("photos") Call<List<Photo>> getPhotos(@Query("page") Integer page, @Query("per_page") Integer perPage, @Query("order_by") String orderBy); @GET("photos/curated") Call<List<Photo>> getCuratedPhotos(@Query("page") Integer page, @Query("per_page") Integer perPage, @Query("order_by") String orderBy); @GET("photos/random") Call<Photo> getRandomPhoto(@Query("collections") String collections, @Query("featured") Boolean featured, @Query("username") String username, @Query("query") String query, @Query("w") Integer width, @Query("h") Integer height, @Query("orientation") String orientation); @GET("photos/random") Call<List<Photo>> getRandomPhotos(@Query("collections") String collections, @Query("featured") boolean featured, @Query("username") String username, @Query("query") String query, @Query("w") Integer width, @Query("h") Integer height, @Query("orientation") String orientation, @Query("count") Integer count); @GET("photos/{id}/download") Call<Download> getPhotoDownloadLink(@Path("id") String id); @GET("search/photos") Call<SearchResults> searchPhotos(@Query("query") String query, @Query("page") Integer page, @Query("per_page") Integer perPage, @Query("orientation") String orientation); }
Response Models
Go through the Unsplash API documentation, and create the required response models in a sub-package in your java directory. We will be using only the required models and attributes, not all of those provided by Unsplash.
Photo.java
public class Photo implements Serializable { @SerializedName("id") @Expose private String id; @SerializedName("width") @Expose private Integer width; @SerializedName("height") @Expose private Integer height; @SerializedName("urls") @Expose private Urls urls; public Urls getUrls() { return urls; } public void setUrls(Urls urls) { this.urls = urls; } }
SearchResults.java
public class SearchResults { @SerializedName("total") @Expose private Integer total; @SerializedName("total_pages") @Expose private Integer totalPages; @SerializedName("results") @Expose private List<Photo> results = null; public List<Photo> getResults() { return results; } public Integer getTotal() { return total; } public void setTotal(Integer total) { this.total = total; } }
Urls.java
public class Urls { @SerializedName("raw") @Expose private String raw; @SerializedName("full") @Expose private String full; @SerializedName("regular") @Expose private String regular; @SerializedName("small") @Expose private String small; @SerializedName("thumb") @Expose private String thumb; public String getFull() { return full; } public void setFull(String full) { this.full = full; } public String getRegular() { return regular; } public void setRegular(String regular) { this.regular = regular; } public String getSmall() { return small; } public void setSmall(String small) { this.small = small; } public String getThumb() { return thumb; } public void setThumb(String thumb) { this.thumb = thumb; } }
Download.java
public class Download { @SerializedName("url") @Expose private String url; }
Preparing the layout
We need square images for our grid. Let’s make a class extending from ImageView to have equal dimensions.
SquareImageView.java
import android.content.Context; import android.util.AttributeSet; import android.widget.ImageView; public class SquareImageView extends ImageView { public SquareImageView(Context context) { super(context); } public SquareImageView(Context context, AttributeSet attrs) { super(context, attrs); } public SquareImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = width; if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec)); } setMeasuredDimension(width, height); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } }
Layout for our activity.
act_img_picker.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:animateLayoutChanges="true"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar_layout" android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" app:theme="@style/MyToolbarStyle" app:layout_scrollFlags="scroll|enterAlways" /> </android.support.design.widget.AppBarLayout> <FrameLayout android:id="@+id/searchLayout" android:layout_width="match_parent" android:layout_height="56dp" android:background="@color/colorPrimary" android:elevation="3dp" android:visibility="gone"> <EditText android:id="@+id/searchBar" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="top" android:layout_marginBottom="2dp" android:layout_marginRight="48dp" android:layout_marginLeft="16dp" android:layout_marginTop="8dp" android:ems="10" android:hint="Search" android:imeOptions="actionDone" android:inputType="text" android:textColor="@android:color/white" android:textColorHint="#cdcdcd" /> <ImageView android:id="@+id/imgCancel" android:src="@android:drawable/ic_menu_close_clear_cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|center_vertical" android:layout_marginRight="16dp" android:onClick="hideSearchBar"/> </FrameLayout> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" /> <ProgressBar android:id="@+id/progressBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:indeterminate="true" android:elevation="2dp" android:background="@android:color/transparent" style="?android:attr/progressBarStyleHorizontal" /> </FrameLayout> </LinearLayout>
Layout for the image item
item_image.xml
We will use our SquareImageView class instead of regular ImageView
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.yourapp.models.SquareImageView android:id="@+id/imgview" android:scaleType="centerCrop" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>
res/menu/menu_unsplash_picker.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_search" android:title="Search" android:orderInCategory="100" android:icon="@android:drawable/ic_menu_search" app:showAsAction="ifRoom" /> </menu>
The adapter
public class PhotosAdapter extends RecyclerView.Adapter<PhotosAdapter.ViewHolder> { private final List<Photo> photoList; private Context mContext; private OnPhotoClickedListener mListener; public PhotosAdapter(List<Photo> photos, Context context, OnPhotoClickedListener listener) { photoList = photos; mContext = context; mListener = listener; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_image, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(final ViewHolder holder, final int position) { Photo photo = photoList.get(position); holder.imageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mListener.photoClicked(photoList.get(holder.getAdapterPosition()), (ImageView)v); } }); Picasso.with(mContext) .load(photo.getUrls().getRegular()) .resize(300, 300) .centerCrop() .into(holder.imageView); } public void addPhotos(List<Photo> photos){ int lastCount = getItemCount(); photoList.addAll(photos); notifyItemRangeInserted(lastCount, photos.size()); } @Override public int getItemCount() { return photoList.size(); } public class ViewHolder extends RecyclerView.ViewHolder { public final ImageView imageView; public ViewHolder(View view) { super(view); imageView = view.findViewById(R.id.imgview); } } public interface OnPhotoClickedListener { void photoClicked(Photo photo, ImageView imageView); } }
For lazy-loading of more images upon scrolling, we will create a scroll listener.
EndlessRecyclerViewScrollListener.java
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener { // The minimum amount of items to have below your current scroll position // before loading more. private int visibleThreshold = 5; // The current offset index of data you have loaded private int currentPage = 0; // The total number of items in the dataset after the last load private int previousTotalItemCount = 0; // True if we are still waiting for the last set of data to load. private boolean loading = true; // Sets the starting page index private int startingPageIndex = 0; RecyclerView.LayoutManager mLayoutManager; public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) { this.mLayoutManager = layoutManager; } public EndlessRecyclerViewScrollListener(GridLayoutManager layoutManager) { this.mLayoutManager = layoutManager; visibleThreshold = visibleThreshold * layoutManager.getSpanCount(); } public EndlessRecyclerViewScrollListener(StaggeredGridLayoutManager layoutManager) { this.mLayoutManager = layoutManager; visibleThreshold = visibleThreshold * layoutManager.getSpanCount(); } public int getLastVisibleItem(int[] lastVisibleItemPositions) { int maxSize = 0; for (int i = 0; i < lastVisibleItemPositions.length; i++) { if (i == 0) { maxSize = lastVisibleItemPositions[i]; } else if (lastVisibleItemPositions[i] > maxSize) { maxSize = lastVisibleItemPositions[i]; } } return maxSize; } // This happens many times a second during a scroll, so be wary of the code you place here. // We are given a few useful parameters to help us work out if we need to load some more data, // but first we check if we are waiting for the previous load to finish. @Override public void onScrolled(RecyclerView view, int dx, int dy) { int lastVisibleItemPosition = 0; int totalItemCount = mLayoutManager.getItemCount(); if (mLayoutManager instanceof StaggeredGridLayoutManager) { int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null); // get maximum element within the list lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions); } else if (mLayoutManager instanceof GridLayoutManager) { lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition(); } else if (mLayoutManager instanceof LinearLayoutManager) { lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition(); } // If the total item count is zero and the previous isn't, assume the // list is invalidated and should be reset back to initial state if (totalItemCount < previousTotalItemCount) { this.currentPage = this.startingPageIndex; this.previousTotalItemCount = totalItemCount; if (totalItemCount == 0) { this.loading = true; } } // If it’s still loading, we check to see if the dataset count has // changed, if so we conclude it has finished loading and update the current page // number and total item count. if (loading && (totalItemCount > previousTotalItemCount)) { loading = false; previousTotalItemCount = totalItemCount; } // If it isn’t currently loading, we check to see if we have breached // the visibleThreshold and need to reload more data. // If we do need to reload some more data, we execute onLoadMore to fetch the data. // threshold should reflect how many total columns there are too if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) { currentPage++; onLoadMore(currentPage, totalItemCount, view); loading = true; } } // Call this method whenever performing new searches public void resetState() { this.currentPage = this.startingPageIndex; this.previousTotalItemCount = 0; this.loading = true; } // Defines the process for actually loading more data based on page public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view); }
Loading images in the picker activity
We will load latest images into our recyclerview when the search is inactive, and when the user searches using some keyword, the search results will be shown.
public class PhotoPickerActivity extends AppCompatActivity { private int page = 1; FrameLayout searchLayout; AppBarLayout appBarLayout; EditText searchBar; RecyclerView recyclerView; ProgressBar progressBar; PhotosAdapter adapter; PhotosAdapter.OnPhotoClickedListener photoClickListener; UnsplashInterface dataService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.act_img_picker); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); appBarLayout = findViewById(R.id.app_bar_layout); searchLayout = findViewById(R.id.searchLayout); searchBar = findViewById(R.id.searchBar); recyclerView = findViewById(R.id.recyclerView); progressBar = findViewById(R.id.progressBar); dataService = UnsplashClient.getUnsplashClient().create(UnsplashInterface.class); searchBar.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if ( (actionId == EditorInfo.IME_ACTION_DONE) || ((event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN ))){ search(searchBar.getText().toString()); return true; } else{ return false; } } }); photoClickListener = new PhotosAdapter.OnPhotoClickedListener() { @Override public void photoClicked(Photo photo, ImageView imageView) { Intent intent = new Intent(); intent.putExtra("image", photo); setResult(RESULT_OK, intent); finish(); } }; GridLayoutManager layoutManager = new GridLayoutManager(this, 2); recyclerView.setLayoutManager(layoutManager); adapter = new PhotosAdapter(new ArrayList<Photo>(), this, photoClickListener); recyclerView.setAdapter(adapter); recyclerView.addOnScrollListener(new EndlessRecyclerViewScrollListener(layoutManager) { @Override public void onLoadMore(int page, int totalItemsCount, RecyclerView view) { loadPhotos(); } }); loadPhotos(); } private void loadPhotos() { progressBar.setVisibility(View.VISIBLE); dataService.getPhotos(page,null,"latest") .enqueue(new Callback<List<Photo>>() { @Override public void onResponse(Call<List<Photo>> call, Response<List<Photo>> response) { List<Photo> photos = response.body(); Log.d("Photos", "Photos Fetched " + photos.size()); //add to adapter page++; adapter.addPhotos(photos); recyclerView.setAdapter(adapter); progressBar.setVisibility(View.GONE); } @Override public void onFailure(Call<List<Photo>> call, Throwable t) { progressBar.setVisibility(View.GONE); } }); } public void search(String query){ if(query != null && !query.equals("")) { progressBar.setVisibility(View.VISIBLE); dataService.searchPhotos(query,null,null,null) .enqueue(new Callback<SearchResults>() { @Override public void onResponse(Call<SearchResults> call, Response<SearchResults> response) { SearchResults results = response.body(); Log.d("Photos", "Total Results Found " + results.getTotal()); List<Photo> photos = results.getResults(); adapter = new PhotosAdapter(photos, PhotoPickerActivity.this, photoClickListener); recyclerView.setAdapter(adapter); progressBar.setVisibility(View.GONE); } @Override public void onFailure(Call<SearchResults> call, Throwable t) { Log.d("Unsplash", t.getLocalizedMessage()); progressBar.setVisibility(View.GONE); } }); } else { loadPhotos(); } } private void showSearchBar() { appBarLayout.setVisibility(View.GONE); searchLayout.setVisibility(View.VISIBLE); searchBar.requestFocus(); } public void hideSearchBar(View view) { searchLayout.setVisibility(View.GONE); appBarLayout.setVisibility(View.VISIBLE); searchBar.clearFocus(); getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_unsplash_picker, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if(item.getItemId() == R.id.action_search) { Log.d("picker", "Search bar open"); showSearchBar(); return true; } if(item.getItemId() == android.R.id.home) { super.onBackPressed(); return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { if(searchLayout.getVisibility() == View.VISIBLE){ Log.d("picker", "Search bar visible"); hideSearchBar(null); return; } super.onBackPressed(); } }
—
Leave A Comment