Creating an Android Image Picker using Unsplash photos

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();
    }
}

 


Also published on Medium.

By |2019-04-03T03:59:00+00:00March 14th, 2019|Categories: Android Tutorials|0 Comments

Leave A Comment