Monday, March 13, 2017

Using Local Json File To Load Data

Some relatively small amounts of data can be stored as json files in the assets directory. This makes the data easier to update than packaging it in an app database or hard coding it in Java objects. I used json to store information about the OpenStax books that are displayed when the app opens. A snippet of the json is below:


{ "bookList":[ { "bookTitle":"College Physics", "title":"College Physics", "contentString":"This introductory, algebra-based, two-semester college physics book is grounded with real-world examples, illustrations, and explanations to help students grasp key, fundamental physics concepts. This online, fully editable and customizable title includes learning objectives, concept questions, links to labs and simulations, and ample practice opportunities to solve traditional physics application problems.", "url":"http://cnx.org/contents/Ax2o07Ul:HR_VN3f7?minimal=true", "bookUrl":"http://cnx.org/contents/Ax2o07Ul:HR_VN3f7?minimal=true", "icon":"physics" }, { "bookTitle":"Introduction to Sociology 2e", "title":"Introduction to Sociology 2e", "contentString":"Introduction to Sociology 2e adheres to the scope and sequence of a typical introductory sociology course. In addition to comprehensive coverage of core concepts, foundational scholars, and emerging theories we have incorporated section reviews with engaging questions, discussions that help students apply the sociological imagination, and features that draw learners into the discipline in meaningful ways", "url":"http://cnx.org/contents/AgQDEnLI:TrIRM88K?minimal=true", "bookUrl":"http://cnx.org/contents/AgQDEnLI:TrIRM88K?minimal=true", "icon":"sociology2e" }, { "bookTitle":"Biology", "title":"Biology", "contentString":"Biology is designed for multi-semester biology courses for science majors. It is grounded on an evolutionary basis and includes exciting features that highlight careers in the biological sciences and everyday applications of the concepts at hand.", "url":"http://cnx.org/contents/GFy_h8cu:rZudN6XP?minimal=true", "bookUrl":"http://cnx.org/contents/GFy_h8cu:rZudN6XP?minimal=true", "icon":"biology" }, ... ] }

The json is stored in the assets folder inside the src/main directory. I created 2 objects to deal with the json file: BookList which contains an ArrayList of Content objects. Each Content object is a book from the list. Gson is the library used to read the json file into a BookList object. Gson must be added to the gradle.build file


compile 'com.google.code.gson:gson:2.7'

The json file is read in a Fragment.


private BookList readJson()
{
    AssetManager assets = getActivity().getAssets();
    BookList bookList = new BookList();

    Gson gson = new Gson();

    try
    {
        InputStream is = assets.open("bookList.json");
        BufferedReader bf = new BufferedReader(new InputStreamReader(is));
        bookList = gson.fromJson(bf,BookList.class);
    }
    catch(IOException ioe)
    {
        Log.d("json", "Some problem: " + ioe.toString());
    }

    Collections.sort(bookList.getBookList());

    return bookList;
}

The data in the objects is used to populate a RecyclerViewAdapter.


@Override
public void onBindViewHolder(final ViewHolder holder, int position)
{
    Content book = contentList.get(position);
    holder.bookTitle.setText(book.getBookTitle());
    if (holder.logo != null && book.getIcon() != null)
    {
        holder.logo.setImageResource(OSCUtil.getCoverId(book.getIcon(), context));

    }
}

The source code for the OpenStax app is available on Github. Look for the code discussed here in the LandingListFragment and LandingListRecyclerViewAdapter classes.

Thursday, January 26, 2017

Remember Where Readers Left Off

Apps that allow users to read books need to remember where the reader left off the last time the book was read. In Android, this is very easy to do using SharedPreferences.

Because of the Android lifecycle, there are 3 places to deal with the previous location.

1. In the onPause() method, store the current URL in SharedPreferences using the name of the icon as a unique identifier.

@Override
protected void onPause()
{
    super.onPause();
    SharedPreferences sharedPref = getSharedPreferences(getString(R.string.osc_package),MODE_PRIVATE);
    SharedPreferences.Editor ed = sharedPref.edit();
    if(webView != null && content != null)
    {
        String url = webView.getUrl().replace("?bookmark=1", "");
        ed.putString(content.getIcon(), url);
        ed.apply();
    }
}

2. In onCreate(), SharedPreferences is checked for a previous URL. If one is available, it is used.

SharedPreferences sharedPref = getSharedPreferences(getString(R.string.osc_package), MODE_PRIVATE);
String url = sharedPref.getString(content.getIcon(), "");

3. In onResume(), SharedPreferences is checked for a previous URL. If one is available, it is used.

It is very simple solution that allows the reader to continue where they left off without creating a bookmark. The code is in the WebViewActivity class of the OpenStax app. The code is available on Github .

Monday, November 21, 2016

Bottom Placement of Floating Action Button (FAB)

I recently wrote a new app and converted 2 existing apps to Material Design. Once of the things I had trouble figuring out was how to get the Floating Action Button (FAB) to be located at the lower right. It was simple once I found the solution.

Using a CoordinatorLayout for the Activity, the FAB should be the last item in the layout and have a layout_gravity of "bottom|right"
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:id="@+id/note_root_layout"
    tools:context="org.openstaxcollege.android.activity.NoteEditorActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_noteeditor"/>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/ic_action_accept"
        android:layout_gravity="bottom|right"/>

</android.support.design.widget.CoordinatorLayout>

A working example is in the OpenStax Android app. Look at the NoteEditorActivity.

Saturday, August 13, 2016

Making NestedScrollView Scroll Smoothly

I recently wrote my first app, OpenStax CNX Music, using CoordinatorLayout and Material Design. I used the Android Studio project template to help me get started which was also a first. It was both a good and a bad idea. It was good because it set me on the right path. The bad was that I had to think differently about how to structure layouts and code. Not really bad, but a learning experience. One of the problems I had was the janky scrolling when using a RecyclerView in a NestedScrollView. I finally found a solution.

In LandingListFragment.onCreate(), I disabled nested scrolling on the RecyclerView.

recyclerView.setNestedScrollingEnabled(false);

OpenStax CNX Music is in the Play Store and the code is open source in Github.

Monday, July 6, 2015

Change APK File Name in Gradle

By default, the Gradle build in Android Studio names the .apk file app-release.apk. With a few small changes to the app build.gradle file, the .apk name can be changed to <app name>-release-<version>.apk.

The file that needs to be edited is the application build.gradle file. It is located in the app directory of your Android Studio project.

First step is to set project.archivesBaseName to your application name.


android {
    compileSdkVersion 21
    buildToolsVersion "21.1.2"
    project.archivesBaseName = "AppName";

Second step is to add some code to the buildTypes section of the Gradle file

buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
            applicationVariants.all { variant ->
                variant.outputs.each{ output ->
                    output.outputFile = new File(output.outputFile.parent,output.outputFile.name.replace(".apk","-" + defaultConfig.versionName + ".apk" ))
                }
            }
        }
    }

And that is all it takes to rename the .apk file.

Sunday, September 21, 2014

Migrate a Project in Github to Android Studio

Android Studio is the IDE of the future for Android development.  Unlike Eclipse, Android Studio and the Gradle build system require a default folder layout for the build system to work out of the box.  If you have stored your project in Github using the Eclipse folder layout, migrating to Android Studio can be a challenge.  This documents how I have done the migration on several projects.  The technic used preserves the Git history as you move the files to the Android Studio folder layout.

Create Empty Android Studio Project

Android Studio will not migrate a project from the Eclipse layout to the Studio format. You will need to do the migration by hand. Creating an empty project will generate some of the Gradle files you will need.  Once you create the project, close Android Studio and rename the project folder.

Create project with Github Checkout



Reopen Android Studio and select the option to Checkout From Source Control.  Enter your Github credintials if you have not already.  The result will be a project with the Eclipse folder layout that will not build and is not tied to Gradle

Move Your Files

Close Android Studio again and return to the command line. Create folders to match the structure of your empty Android Studio project. The folders you will have to create are

app/src/main/java

Use Git to move the Java source files to the app/src/main/java directory

git mv src/org app/src/main/java


Next, move the res folder

git mv res app/src/main


Finally, move AndroidManifest.xml

git mv AndroidManifest.xml app/src/main

Copy Files from Empty Project

Copy these files and directories to the same location in your Github checkout project.

  •  gradle directory
  •  gradle.properties
  •  gradlew
  •  build.gradle
  •  app/build.gradle

Migrating to Gradle Build

 Open the Github project in Android Studio.  The Gradle files will be detected and a popup will ask you if you want to migrate to a Gradle project. Click the link and the project will be updated.  If your project will still not build, you may need to add dependencies for libraries such as Appcompat in the app/build.gradle file.

Saturday, April 26, 2014

GridView for Multiple Screen Sizes



For the OpenStax College app, I needed to display the book covers in a GridView which sounds like an easy task, but it was not.  The main problem was that the book covers are rectangular and they could not be cropped.  As with all things Android, the grid needed to deal with different screen sizes correctly.  The code I will detail below adjusts the number of items in the grid based on screen size and orientation (landscape vs. portrait). I was helped tremendously by this post.

Layouts for the grid involve two XML files gridview.xml and gridcell.xml

gridview.xml

<framelayout android:background="@color/beige" android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android">
    <gridview android:columnwidth="200dp" android:gravity="center" android:horizontalspacing="10dp" android:id="@+id/gridView" android:layout_height="match_parent" android:layout_width="match_parent" android:numcolumns="2" android:padding="5dp" android:stretchmode="columnWidth" android:verticalspacing="10dp">

</gridview></framelayout>

gridcell.xml

<framelayout android:gravity="center" android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android">
 
    <org .openstaxcollege.android.views.oscimageview="" android:id="@+id/grid_item_image" android:layout_gravity="center" android:layout_height="200dp" android:layout_margin="5dp" android:layout_width="200dp" android:scaletype="fitCenter" android:src="@drawable/biology_lg">
    </org>
 
</framelayout>

gridcell.xml references a custom ImageView class, OSCImageView. This class gives some control over how the image is displayed in the onMeasure() method.

public class OSCImageView extends ImageView 
{
 public OSCImageView(Context context) 
 {
        super(context);
    }

    public OSCImageView(Context context, AttributeSet attrs) 
    {
        super(context, attrs);
    }

    public OSCImageView(Context context, AttributeSet attrs, int defStyle) 
    {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); 
    }

}

There are two inner classes in my opening activity which work as part of the GridView. The first, BookCover, is a simple javabean to hold the info about each cover.

private class Bookcover
{
     
  final String name;
  final int drawableId;

  Bookcover(String name, int drawableId) 
  {
     this.name = name;
     this.drawableId = drawableId;
  }
        
    
}

I originally displayed the title of each book with the cover, but decided to do away with the title for now. I left the name field since we may have covers in the future that do not display the title as well as the current covers do.

The other inner class is ImageAdaptor which holds all of the covers and displays them in the GridView.

    
class ImageAdapter extends BaseAdapter 
    {
     private Context context;
     
     List<bookcover> bookcovers = new ArrayList<bookcover>();
     
     public ImageAdapter(Context c)
     {
      context = c;

            bookcovers.add(new Bookcover("",R.drawable.physics_lg));
            bookcovers.add(new Bookcover("",R.drawable.sociology_lg));
            bookcovers.add(new Bookcover("", R.drawable.biology_lg));
            bookcovers.add(new Bookcover("",R.drawable.concepts_biology_lg));
            bookcovers.add(new Bookcover("",R.drawable.anatomy_lg));
            bookcovers.add(new Bookcover("",R.drawable.statistics_lg));
            bookcovers.add(new Bookcover("",R.drawable.precalculus_lg));
            bookcovers.add(new Bookcover("",R.drawable.psychology_lg));
            bookcovers.add(new Bookcover("",R.drawable.econ_lg));
            bookcovers.add(new Bookcover("",R.drawable.chemistry_lg));
            bookcovers.add(new Bookcover("",R.drawable.history_lg));
            bookcovers.add(new Bookcover("",R.drawable.macro_econ_lg));
            bookcovers.add(new Bookcover("",R.drawable.micro_econ_lg));
      
     }

     /* (non-Javadoc)
      * @see android.widget.Adapter#getCount()
      */
     @Override
     public int getCount() 
     {
      return bookcovers.size();
     }

     /* (non-Javadoc)
      * @see android.widget.Adapter#getItem(int)
      */
     @Override
     public Object getItem(int position) 
     {
      return bookcovers.get(position);
     }

     /* (non-Javadoc)
      * @see android.widget.Adapter#getItemId(int)
      */
     @Override
     public long getItemId(int position) 
     {
      return 0;
     }

     /* (non-Javadoc)
      * @see android.widget.Adapter#getView(int, android.view.View, android.view.ViewGroup)
      */
     @Override
     public View getView(int position, View convertView, ViewGroup parent) 
     {

      View v = convertView;
            ImageView picture;
            //TextView name;

            if(v == null) {
             
                v = LayoutInflater.from(context).inflate(R.layout.gridcell, parent, false);
                v.setTag(R.id.grid_item_image, v.findViewById(R.id.grid_item_image));
                //v.setTag(R.id.text, v.findViewById(R.id.text));
            }

            picture = (ImageView)v.getTag(R.id.grid_item_image);
            //name = (TextView)v.getTag(R.id.text);

            Bookcover item = (Bookcover)getItem(position);

            picture.setImageResource(item.drawableId);
            //name.setText(item.name);

            return v;
     }

    }

In the Activity onCreate() method, The GridView is setup for orientation and screen size.

        
GridView gridView = (GridView) findViewById(R.id.gridView);
        int orient = getResources().getConfiguration().orientation;
        Display d = getWindowManager().getDefaultDisplay();
        boolean isTablet = OSCUtil.isTabletDevice(this);
        if(orient == Configuration.ORIENTATION_LANDSCAPE && isTablet)
        {
            if(OSCUtil.isXLarge(this))
            {
                gridView.setNumColumns(5);
            }
            else
            {
             gridView.setNumColumns(4);
            }
        }
        else if(orient == Configuration.ORIENTATION_LANDSCAPE)
        {
         gridView.setNumColumns(3);
        }
        else if(orient == Configuration.ORIENTATION_PORTRAIT && isTablet)
        {

            if(OSCUtil.isXLarge(this))
            {
                gridView.setNumColumns(4);
            }
            else
            {
                gridView.setNumColumns(3);
            }
        }
        
        gridView.setAdapter(new ImageAdapter(this));
        gridView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView parent, View v,int position, long id) {
             if(position > 5)
             {
              Toast.makeText(LandingActivity.this, getString(R.string.coming_soon),  Toast.LENGTH_SHORT).show();
              return;
             }
             Content c = content.get(position);
             ContentCache.setObject(getString(R.string.webcontent), c);
             startActivity(new Intent(getApplicationContext(), WebViewActivity.class));
                
            }
        });


The number of columns is adjusted based on the screen size and orientation. Code to determine if the device is a tablet or not was covered in this post. When a book is selected in the Grid, if it is one of the first 5, the Content object is used to send the user to the book URL. If the selected cover is not in the first 5, a Toast is displayed. I'm not sure why I coded the URL as a separate object and it could probably be refactored to have the URL in the BookCover object. The Content object is most likely a leftover from one of the many things I tried that did not work.

The code for the OpenStax College app is available on Github. The App is available in Google Play.