Menu

170829Ago - CHAPTER 7

170829Ago - CHAPTER 7. Making a ListView!

Hello, last day we did the checking for have the correct permissions to access to the device storage.

As we can read the file structure of the device we can show that information to the user.

For that we need a UI a little more complex that a simple text on the screen. Too we'll need to programm the user interactivity, when the user to click on folders we must do the navigation into our File Browser.

We will need a ListView, this is an Android component that shows a visual line for each item of a Data Array.

This Data Array will contains a item for each file or folder in our current directory.

I don't explain here how read folders in java, I'll assume that you know how do that.

I go to make a normal java class to work with that: MyFileBrowser this contains the logic to read folders and update the ViewList.

Go first with this work, the ViewList is a dynamic component, unlike previous ScrollViews that has static content defined in a layout.

In this case is in run-time when we know the folder to show when we can fill the ListView.

I added this XML definition in activity_main:

<ListView android:visibility="invisible"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:id="@+id/list_view"
        app:layout_constraintTop_toTopOf="@+id/top_guideline"
        app:layout_constraintBottom_toTopOf="@+id/bottom_guideline"
        app:layout_constraintRight_toLeftOf="@+id/right_guideline"
        app:layout_constraintLeft_toLeftOf="@+id/left_guideline">
</ListView>

This is the definition of the container, that is, the ListView.

We will need too another XML definition for each item in the list. But this go in other XML file, I called it item_files.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/itemFiles">

    <ImageView
        android:id="@+id/file_icon"
        android:layout_gravity="center"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:textColor="@android:color/black"
        android:src="@drawable/ic_option_refresh" />

    <TextView
        android:id="@+id/file_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="4dp"
        android:layout_gravity="left"
        android:layout_toRightOf="@+id/file_icon"
        android:text="File name"
        android:textColor="@android:color/black"
        android:textSize="14dp" />
    <TextView
        android:id="@+id/file_attr"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="8dp"
        android:layout_gravity="right"
        android:layout_toRightOf="@+id/file_icon"
        android:layout_below="@+id/file_icon"
        android:text="File attribs"
        android:textColor="@android:color/black"
        android:textSize="10dp" />
</RelativeLayout>

This define an icon for differentiate folder and files. And two TextView one with a textSize more big for the name of the folder/file and other for the attributes of the file (and file size)

We need too images to charge in the ImageView. This images can be included in out app as resource files. This go in the res/drawable folder.

To add images to this folder you must do right click over "drawable" and select in the sub-menu "New/Image Asset".

Then appear this dialog. Select in the Icon Type: "Action Bar and Tab Icons". If not the size and type of the image become very big.

Then do click on the button "Clip Art" and look the the image that you want.

When finish you have several images with different sizes for the image icon, but you don't worry for that, Android will manage it automaticaly selecting the apropiate image for the resolution of each device.

At now we have:

  • A ListView in the main_activity.xml
  • A item_files.xml with the design for each item.
  • The resource images in res/drawable.
  • A java class for the login to manage File objects: MyFileBrowser.

We will need something more, a specific Adaptor class. It control the item to show.

package homebrew.dhtejada.androidblog.mysuperapp.adapters;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import android.view.View.OnClickListener;
import homebrew.dhtejada.androidblog.mysuperapp.R;

public class MyAdapter4Files extends BaseAdapter {
    private static final String TAG = "MyAdapter4Files_1.0";

    String [] result;
    OnClickListener listener;
    Context context;
    int [] imageId;
    private static LayoutInflater inflater=null;
    public MyAdapter4Files(OnClickListener listener, Context context, String[] prgmNameList, int[] prgmImages) {
        // TODO Auto-generated constructor stub
        result=prgmNameList;
        this.listener=listener;
        this.context=context;
        imageId=prgmImages;
        inflater = ( LayoutInflater )context.
                getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public void refreshData(String[] prgmNameList){
        this.result=null;
        this.result=prgmNameList;
        notifyDataSetChanged();
    }

    @Override
    public int getCount() { return (result==null)? 0 : result.length; }

    @Override
    public Object getItem(int position) { return position; }

    @Override
    public long getItemId(int position) {return position; }

    public class Holder{
        int position;
        TextView tvn;
        TextView tva;
        ImageView img;
        public boolean isDirectory;
        public String getName(){ return tvn.getText().toString(); }
        public boolean isDirectory(){ return isDirectory; }
        public int getPosition(){ return position; }
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // TODO Auto-generated method stub
        Holder holder=new Holder();
        View rowView;
        rowView = inflater.inflate(R.layout.item_files, null);
        holder.position=position;
        holder.tvn=(TextView) rowView.findViewById(R.id.file_name);
        holder.tva=(TextView) rowView.findViewById(R.id.file_attr);
        holder.img=(ImageView) rowView.findViewById(R.id.file_icon);

        rowView.setTag(holder);

        String line="UNKNOWN";

        line=result[position];
        if (line!=null) {
            holder.tvn.setText(getName(line));
            holder.tva.setText(getAttr(line));
            holder.isDirectory = isDirectory(line);
            holder.img.setImageResource(imageId[ (holder.isDirectory ? 0 : 1)]);
            rowView.setOnClickListener(listener);
        }else{
            Log.e(TAG, "Line null. position: "+position);
        }

        return rowView;
    }

    // Work methods to parse eac line of data array that has the format:
    //      DAAA \t SIZE \t NAME

    boolean isDirectory(String line){ return line.startsWith("d"); }

    String getName(String line){
        String[] tokens = line.split("\t");
        return  tokens[2];
    }

    String getAttr(String line){
        String[] tokens = line.split("\t");
        return  tokens[0] + "\t" + tokens[1];
    }
}

Well, the adapter constructor receives a listener (that will be our class MyFileBrowser) to manage the user Clicks.

Receives too, an array with the file names in the folder to show and an array with the images ids (integers) of the images for the icons.

The ListView object calls to the method getView for each row in the data array (It know how many items there are, thanks to the method getCount)

We make a view calling to inflater, this use the design for the view item:

View rowView;
    rowView = inflater.inflate(R.layout.item_files, null);

More later we pass to this item view the listener that will be called when the user make click:

rowView.setOnClickListener(listener);

We have a method refreshData to said to the Adapter that the data array was changed due to the user did click on a folder.

This adapter is created when we show the ListView first time in our MyFileBrowser:

// in the MyFileBrowser class [...]

    public void refresh(){
        Log.d(TAG, "refresh!");
        fileNames=null;
        File[] content=fileCmder.pwd.listFiles();
        if (content!=null) {
            fileNames = new String[content.length];
            int i = 0;

            for (File f : content) { // Puts first directories
                if (f.isDirectory()) fileNames[i++] = expand(f);
            }
            for (File f : content) { // Puts after files
                if (f.isFile()) fileNames[i++] = expand(f);
            }
        }
        if (myFileAdapter==null) {
            myFileAdapter = new MyAdapter4Files(listener, context, fileNames, fileIcons);
            listView.setAdapter(myFileAdapter);
        }else {
            myFileAdapter.refreshData(fileNames);
        }
        int firstPosition = listView.getFirstVisiblePosition();
        listView.setSelection(firstPosition);
        currentFolder.setText(fileCmder.pwd.getAbsolutePath());
    }

Look how the adapter is passed to the listView object if is the first time or how the method refreshData is called if is a change of content.

The listener is a reference to this class taken in the constructor:

this.listener=this;
public class MyFileBrowser implements View.OnClickListener, MyFileCmdCaller{
    private static final String TAG = "MyFileBrowser_1.0";

    [...]

    Context context=null;
    static View.OnClickListener listener=null;
    ListView listView=null;
    TextView currentFolder;
    MyAdapter4Files myFileAdapter=null;
    Spinner spinner=null;

    ArrayList<String> messages=null;
    String lastMsg="None";
    MyFileCmdBuilder fileCmder;

    public static int[] fileIcons ={R.drawable.ic_item_folder, R.drawable.ic_item_file};
    public static String[] fileNames=null;

    MyFileBrowser(Context context, ListView lv, TextView currentFolder, Spinner spinner){
        this.context=context;
        this.listener=this;

        listView=lv;
        this.currentFolder=currentFolder;
        this.spinner=spinner;

        messages=new ArrayList<String>();
        messages.add(TAG);
        fileCmder=new MyFileCmdBuilder(this)
                .PWD()
                .CD("/")
                .DIR();
        refresh();
    }

    [...]

Here we to declare the fileNames array that we will charge with data and pass it to the adapter.

Look too how the image icons ids array is declared with a icon [0] for folders and another [1] for files:

public static int[] fileIcons ={R.drawable.ic_item_folder, R.drawable.ic_item_file};

The class MyFileCmdBuilder is a helper class for MyFileBrowser based in the Command Pattern to work with files and folders.

More later I'll add to this class commands to copy, delete, and move files and folders, I want too add it do/undo facilities.

Now I go to add to the options bar in the upper area some buttons with basic commands:

This buttons are static and are included from activity_main.xml

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

This file options_files.xml has:

<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:visibility="invisible"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:keepScreenOn="true"
    android:scrollbarStyle="insideInset"
    android:id="@+id/options_files"
    android:scrollbars=""
    app:layout_constraintBottom_toTopOf="@+id/top_guideline"
    app:layout_constraintLeft_toRightOf="@+id/left_guideline"
    app:layout_constraintRight_toLeftOf="@+id/right_guideline"
    tools:layout_editor_absoluteX="8dp">
    <LinearLayout android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ImageButton
                android:id="@+id/btn_files_refresh"
                android:layout_width="40dp"
                android:layout_height="40dp"
                app:srcCompat="@drawable/ic_option_refresh"
                tools:layout_editor_absoluteX="10dp"
                tools:layout_editor_absoluteY="10dp"
                android:onClick="refreshFiles"/>

            <ImageButton
                android:id="@+id/btn_files_back"
                android:layout_width="60dp"
                android:layout_height="40dp"
                app:srcCompat="@drawable/ic_option_back"
                tools:layout_editor_absoluteX="10dp"
                tools:layout_editor_absoluteY="10dp"
                android:onClick="filesBack"/>
            <Spinner
                android:id="@+id/spinner_storage"
                android:layout_width="wrap_content"
                android:layout_height="40dp"
                android:textSize="10dp">
            </Spinner>
        </LinearLayout>
        <TextView
            android:id="@+id/folder_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Current path..."
            android:textStyle="italic"
            android:textAlignment="textStart" />
    </LinearLayout>
</HorizontalScrollView>

Note that the method filesBack call to MainActivity class and this call to MyFileBrowser.back:

    public void back(){
        String parent=fileCmder.pwd.getParent();
        if (parent==null){
            Log.d(TAG,"Skipping Backing! We are in root folder.");
            return;
        }
        Log.d(TAG,"Backing to: "+parent);
        fileCmder.CD(parent).DIR();
        refresh();
    }

We have a spinner object, this load his values from a resource file, res/storages_array.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="storages_array">
        <item>Root</item>
        <item>Internal</item>
        <item>External</item>
    </string-array>
</resources>

The MainActivity class respond to the button "Files" with the method showFiles:

public void showFiles(View view) {
        showOptions(R.id.options_files);
        showListContent(null); // Show ListView Content

        if (fileBrowser == null){
            ListView lv = (ListView) findViewById(R.id.list_view);
            TextView currentFolder=(TextView) findViewById(R.id.folder_text);
            Spinner spinner = (Spinner) findViewById(R.id.spinner_storage);

            ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
                    R.array.storages_array, android.R.layout.simple_spinner_item);
            adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

            spinner.setAdapter(adapter);
            spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
                    @Override
                    public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
                            Log.d(TAG, "File spinner: " + pos);
                            fileBrowser.select(pos);
                        }
                    public void onNothingSelected(AdapterView<?> parent) {
                        // Another interface callback
                    }
                });

            fileBrowser = new MyFileBrowser(getApplicationContext(), lv, currentFolder, spinner);
        }
    }

The spinner has too an Adapter but we can use one created by the Android API that take the values from our storages_array:

ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.storages_array, android.R.layout.simple_spinner_item);

No more for today, bye!

Posted by David Hernandez Tejada 2017-08-29 Labels: ANDROID STUDIO ANDROID PROGRAMMING DEVELOP LIST VIEW

Log in to post a comment.

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.