Pages

Monday, 12 January 2015

In Android app Screen-shots and ListView tricks

Have you ever used an app that lists something and you want to share a certain list-item on it, in it's as is format - like a picture of the entire list item? But, all your sharing options seems to just extract the text from the list item and add some streams of information to share in a weird text format with other Sharing Apps on your Android device.

Well, it's of utmost importance that an app that produces content in lists should allow it's list-items be shared as an image. Today I'll be giving a walkthrough of one of the most important features any discussion or list view Android app must have.

Today I'll be giving a walkthrough of one of the most important features any discussion or list view Android app must have.

[Note: Those who don't know about ListViews and ListActivity, should first go through some tutorials by Vogella and the Android Developer Documentation. I'll be covering the details of ListActivity and ListViews in a jiffy.]

A sneak peek at the App to be built

In Android app Screen-shots and ListView tricks
Actual Main Activity using activity_main.xml

Android ListView Example
How a single list item or row looks in the App.

Before we begin...

I'm using the internet to load user images from the web. Your server side team or you yourself can fetch User Profile images as URLs on a web-server. The entire process of requesting the URL corresponding to a certain user is out of the purview of this tutorial. (You might want to look into SOAP or JSON service consumption.)

I am using a very popular third party open source library by Nostra, called the Universal Image Loader to download and display images in Image Views from their respective URLs. You can read more about it on his GitHub [Link]. He is also very keen on answering any questions and clearing doubts in using the Universal Image Loader on Stack Overflow, so don't shy away if you want to learn more about Universal Image Loader. I'll not be going into meticulous detail about using the UIL, the small set of codes that uses the UIL in the Adapter is enough for current usage. But, I still insist to learn more about it as it is a very powerful and useful library.

Now, I'm going to break the over all app into a rudimentary structure:

1. Main Activity - This is responsible for running the app and has the apps main List Activity's view attached to it.

2. Comments - This class is a custom class which we have used to hold, manipulate and populate the Comments used in the app. This makes it a lot easier to handle multiple data in an easy object format, instead of throwing several arrays, arraylists and other variables all over the List Adapter.

You might see this in several examples and tutorials on the web. But, it's highly advised, from my side, to use proper objects for handling data. As programs in real world tend to grow and deal with tens and dozens of variables for single activities and components, it's better to organise them in a neat object oriented design, rather than create a massive mess of code in critical activities and adapters.

3. CommentsAdapter - This is the Adapter class responsible for populating and handling the List and it's behavior. Every List has an Adapter. Sometimes, people build adapters as an inner class to their ListActivity class, in order to keep the content together. But, it's a shoddy practice, as Lists with a wide variety of features tend be code intensive and bulky, they begin to clutter the Activity's code.

It is better to code adapter's separately, which make them easy to handle (think modularity), reusable, and keeping them in a separate package for adapters makes it easy for file keeping (if you have more than one adapter in your app).

4. viewHolder - This class works as a.... well, what it's name suggests; A View Holder. This makes it a lot easy to handle and use the several views used in the list-item, like ImageViews, TextViews etc. This is used to make it easy to refer and handle each list item's components/widgets by their position, as fetched from the Adapter.

Some of the features used in this app needs permissions from the user and requires mentioning in the Manifest. Add the following permissions in the app manifest.


<uses-permission
        android:name="android.permission.INTERNET"
        android:maxSdkVersion="19" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />


Internet permission is required to use the Universal Image Loader to connect with the URL on the web to fetch Images. The Write permissions are used to write/store the screen-shots taken as an image file on the device external storage, to enable it's sharing.

For most professional apps, developers need to explain in detail on their Developer Site as to why the permissions are required and what is the app going to do with it. This is in order to maintain transparency and protect user privacy as well as maintain user's devices security. It is also required as a semi-legal agreement/declaration between the developer and the user. It should be developed as a practice to explain the purposes of varied permissions used in the app.

Let's start first with a UI Layout design;

XML Layout

To build a ListView we'll need to sets of XML Layouts defined: One for the original List and it's Activity and second for each list-item/row component.

Here's a simple ListActivity - (called activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <View
        android:id="@+id/labelviewP"
        android:layout_width="fill_parent"
        android:layout_height="5dp"
        android:layout_alignParentTop="true"                                
        android:background="@color/blue_steel" />
    
    <ListView
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/editText1"
        android:layout_below="@id/labelviewP"
        android:background="@color/blue_steel"
        android:cacheColorHint="@color/blue_steel"
        android:divider="@color/blue_steel"
        android:dividerHeight="2dp"
        android:paddingLeft="5dp"
        android:paddingRight="5dp" />

    <View
        android:id="@+id/labelviewP2"
        android:layout_width="fill_parent"
        android:layout_height="5dp"
        android:layout_above="@+id/editText1"                                
        android:background="@color/blue_steel" />
    
    <Button
        android:id="@id/editText1"
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/round_corner_rectangle"
        android:gravity="center"
        android:minLines="2"
        android:textCursorDrawable="@null"        
        android:hint="Write Comment Here."                                        
        android:textAlignment="center"
        android:textColor="@color/black" />
</RelativeLayout>


Now, let's build each list item. Each list item is supposed to show, the User Image, their name, an advanced Share button and their comment. But, I've taken the liberty of adding a small nifty feature of expandable list items. Such that each comment is cut short when it exceeds a certain length. This shortened comment can be expanded via "Read More" button and then collapsed back to their original shortened state by "Show Less" button.

This feature of collapsible comments is implemented in the lists adapter. We'll get to it a little later. For now, the list-item XML layout is - (called commentlistitem.xml)


<?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"
    android:background="@drawable/round_corner_rectangle_shadowed">

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_alignParentLeft="true"
        android:padding="5dp"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true" />

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/imageView1"
        android:padding="5dp"
        android:text="User Name"
        android:textColor="#087CCD" />

    <ImageView
        android:id="@+id/imageViewShare"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_alignBottom="@id/name"
        android:layout_alignParentRight="true"
        android:padding="2dp"
        android:scaleType="fitCenter"
        android:src="@drawable/abc_ic_menu_share_holo_light"
        android:adjustViewBounds="true" />
    
    <TextView
        android:id="@+id/comment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@id/name"
        android:layout_below="@id/name"
        android:layout_marginTop="2dp"
        android:layout_toRightOf="@+id/imageView1"
        android:padding="5dp"
        android:text="User Comment Here"
        android:textColor="#000" />

    <TextView
        android:id="@+id/readmore"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"        
        android:layout_below="@id/comment"
        android:layout_alignParentRight="true"
        android:padding="10dp"
        android:text="Read More"
        android:textColor="@color/blue_steel"
        android:visibility="invisible"
        android:enabled="false"/>
</RelativeLayout>

Comments Class

Your List can contain anything, from contacts, to notes and high scores for a game. Here, I'm dealing with a hypothetical case of a Forum/Discussion app, where users post comments. Here's the source code:


import java.util.ArrayList;

public class Comments {

 private ArrayList<String> CommentBy = new ArrayList<String>();
 private ArrayList<String> CommentData = new ArrayList<String>();
 private ArrayList<String> UserPicLink = new ArrayList<String>();
 
 //A constructor to be used for actual initialization of object from information fetched from
 //other sources like a web-service or on device storage (SQLite database).
 public Comments(ArrayList<String> commentBy, ArrayList<String> commentData,
   ArrayList<String> userPicLink) {
  super();
  CommentBy = commentBy;
  CommentData = commentData;
  UserPicLink = userPicLink;
 }
 
 //My impromptu constructor used to initialize Comments object with a set of preset sample comments
 //Make sure you remove this constructor when using for actual app.
 public Comments() {
  super();
  
  CommentBy.clear();
  CommentData.clear();
  UserPicLink.clear();

  /*Download the original source code for this class to enjoy a much richer Comments data*/

  //Standard Blue User
  CommentBy.add("Average_Joe");
  CommentData.add("I love seeing movies made from Comics characters!!");
  UserPicLink.add("http://www.psdgraphics.com/file/user-icon.jpg");
  
  //Standard Black User
  CommentBy.add("Naseem");
  CommentData.add("Ssh! This is just a tutorial/demo dont attract attention here.</br></br>You don't know what kind of people will be attracted towards this discussion on the web.");
  UserPicLink.add("http://www.backgroundsy.com/file/preview/user-icon.jpg");
  
  //Joker
  CommentBy.add("Joker");
  CommentData.add("Ooh! A new discussion! Do you want to hear a joke?");
  UserPicLink.add("http://images6.fanpop.com/image/photos/32200000/the-joker-the-joker-32252723-200-200.jpg");
 }
 
 public String getCommentBy(int position) {
  return CommentBy.get(position);
 }
 
 public void setCommentBy(String commentBy, int position) {
  CommentBy.add(position, commentBy);
 }
 
 public String getCommentData(int position) {
  return CommentData.get(position);
 }
 
 public void setCommentData(String commentData, int position) {
  CommentData.add(position, commentData);
 }
 
 public String getUserPicLink(int position) {
  return UserPicLink.get(position);
 }
 
 public void setUserPicLink(String userPicLink, int position) {
  UserPicLink.add(position, userPicLink);
 }

 public int numberOfComments() {
  return CommentBy.size();
 }
 
}

viewHolder and Main Activity Class

Now, the source codes for the viewHolder which is pretty much self explanatory as well as the Main Activity Class.


import android.widget.ImageView;
import android.widget.TextView;

public class viewHolder 
{
 TextView CommentByName;
 TextView CommentTextString;
 ImageView CommentImage;
 TextView CommentReadMore;
 ImageView CommentShare;
}

And, the Main Activity - 


import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.widget.ListView;

import com.naseem.screenshotsharing.Comments;

public class MainActivity extends Activity {

 Context con;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        con = this;
      
  Comments comment = new Comments();
  
  ListView list = (ListView) findViewById(android.R.id.list);
  
  list.setAdapter(new CommentsAdapter(con, comment));
    }
    
}

CommentsAdapter


import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Environment;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class CommentsAdapter extends BaseAdapter {

 private Context context;
 private Comments comments;
 
 LayoutInflater inflater;

 public CommentsAdapter(Context context, Comments incomingComments) {
  this.context = context;
  this.comments = incomingComments;
  inflater = LayoutInflater.from(context);
 }

 @Override
 public int getCount() {
  return comments.numberOfComments();
 }

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

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

 @Override
 public View getView(final int position, View convertView, ViewGroup parent) {
  final viewHolder holder;
  if (convertView == null) {
   convertView = inflater.inflate(R.layout.commentlistitem, null);
   holder = new viewHolder();

   convertView.setTag(holder);

  } else {
   holder = (viewHolder) convertView.getTag();
  }
  holder.CommentByName = (TextView) convertView.findViewById(R.id.name);      
  holder.CommentTextString = (TextView) convertView.findViewById(R.id.comment);      
  holder.CommentImage=(ImageView)convertView.findViewById(R.id.imageView1);   
  holder.CommentReadMore = (TextView) convertView.findViewById(R.id.readmore);
  holder.CommentShare = (ImageView) convertView.findViewById(R.id.imageViewShare);
  
  //Load Image Block
  
  String image_url = comments.getUserPicLink(position);

  if (!comments.getUserPicLink(position).equalsIgnoreCase("null")) {
   ImageLoader imageLoader = ImageLoader.getInstance();
   imageLoader.init(ImageLoaderConfiguration.createDefault(convertView
     .getContext()));

   // Load and display image asynchronously
   DisplayImageOptions options = new DisplayImageOptions.Builder()
     .showStubImage(R.drawable.ic_action_person)
     // this is the image that will be displayed if download
     // fails
     .cacheInMemory().cacheOnDisc().build();

   imageLoader.displayImage(image_url, holder.CommentImage, options);
  }
  
  holder.CommentImage.setOnClickListener(new OnClickListener() {
   public void onClick(View v)
   {
    //TODO Write intent here to open selected user's profile
   }
 
  });
  
                //Count length of Comment text and shorten it if it exceeds 150 characters.
  String commentContent;
  
  boolean ReadMoreStatus = false;
  
  if(Html.fromHtml(comments.getCommentData(position)).toString().length()<151)
  {
   commentContent = comments.getCommentData(position);
   
   ReadMoreStatus = false;
  }
  else
  {
   commentContent = Html.fromHtml(comments.getCommentData(position)).toString();
   commentContent = commentContent.substring(0,150)+"...";
   
   ReadMoreStatus = true;
  }   
  
  holder.CommentByName.setText(comments.getCommentBy(position));
  holder.CommentByName.setOnClickListener(new OnClickListener() {
   public void onClick(View v)
   {
    //TODO Write intent here to open selected user's profile
   }
  });
  
  
  final String fullComment = comments.getCommentData(position);
  if(ReadMoreStatus)
  {
   holder.CommentReadMore.setEnabled(true);
   holder.CommentReadMore.setVisibility(View.VISIBLE);
   
   holder.CommentTextString.setText(commentContent);
   ReadMoreStatus = false;
   
   //Define Click Event of Read More as per the Content of the comment text
      final CommentsAdapter CA = new CommentsAdapter(context, comments);
   holder.CommentReadMore.setOnClickListener(new OnClickListener() {
    public void onClick(View v)
    {
     if(!holder.CommentReadMore.getText().toString().equalsIgnoreCase("Show Less"))
     {
      holder.CommentTextString.setText(Html.fromHtml(fullComment));
      holder.CommentReadMore.setEnabled(true);
      holder.CommentReadMore.setText("Show Less");
      CA.notifyDataSetChanged();
     }
     else
     {
      String s44 = Html.fromHtml(fullComment).toString();
      holder.CommentTextString.setText(s44.substring(0,150)+"...");
      holder.CommentReadMore.setEnabled(true);       
      holder.CommentReadMore.setText("Read More");
      CA.notifyDataSetChanged();
     }
    }
  
   });
       
  }
  else
  {
   holder.CommentReadMore.setEnabled(false);
   holder.CommentReadMore.setVisibility(View.INVISIBLE);
   
   Spanned t10 = Html.fromHtml(comments.getCommentData(position));
   holder.CommentTextString.setText(t10);
  }
  
  //Share Comment Screenshot
  @SuppressWarnings("unused")
  final CommentsAdapter CA = new CommentsAdapter(context, comments);
  final View kV = convertView;
  holder.CommentShare.setOnClickListener(new OnClickListener() {
   public void onClick(View v)
   {
    //Grab Screenshot
    Bitmap bitmap = null;
    bitmap = getBitmapFromView(kV);
    
    //Save this bitmap to a file.
     // Find the SD Card path
             File filepath = Environment.getExternalStorageDirectory();
  
             // Create a new folder AndroidBegin in SD Card
             File dir = new File(filepath.getAbsolutePath() + "/Share Discussion Comment/");
             dir.mkdirs();
  
             // Create a name for the saved image
             File file = new File(dir, "Comment_By_"+holder.CommentByName.getText()+".png");
             OutputStream output;
             
             try {
              output = new FileOutputStream(file);
  
                 // Compress into png format image from 0% - 100%
                 bitmap.compress(Bitmap.CompressFormat.PNG, 100, output);
                 output.flush();
                 output.close();
  
                 // Locate the image to Share
                 Uri uri = Uri.fromFile(file);
  
                 // Share Intent
                 Intent share = new Intent(Intent.ACTION_SEND);

                 // Type of file to share
                 share.setType("image/jpeg");
                 
                 // Pass the image into an Intent
                 share.putExtra(Intent.EXTRA_STREAM, uri);

                 // Pass the origin link into the Intent
                 share.putExtra(Intent.EXTRA_TEXT, "http://www.coders-hub.com/");
                 // Show the social share chooser list
                 context.startActivity(Intent.createChooser(share, "Share Comment Snapshot"));
  
             } catch (Exception e) {
                 e.printStackTrace();
             }
   }
 
  });
  //Sharing Screenshot of Comment
  
  return convertView;
 }
 
 
 //VIEW TO BITMAP CONVERSION - THE KEY TO SCREENSHOTS
        //THIS IS WHERE THE MAGIC HAPPENS.
 public static Bitmap getBitmapFromView(View view)
 {
     Bitmap returnedBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
     Canvas canvas = new Canvas(returnedBitmap);
     Drawable bgDrawable = view.getBackground();
     if (bgDrawable != null)
         bgDrawable.draw(canvas);
     else
         canvas.drawColor(Color.WHITE);
     view.draw(canvas);
     return returnedBitmap;
 }
}


Most of the above code is self explanatory.

Screenshots


Starting Share for a certain comment.
Starting Share for a certain comment.

Attaching shared image to EMail
Attaching shared image to EMail.

Shared image received via email (I used Email. You may use any other app on device.)

Open/Expanded comment shared.
Open/Expanded comment shared.

Collapsed/Simple comment shared.
Collapsed/Simple comment shared.

And, finally the Source Code

You can download the source code via this link - [Download]. Make sure you've downloaded and setup Nostra13's Universal Image Loader, first.

Related Tutorials:-

Create List and perform Actions on it

Create Auto Complete Text

Create Spinner and perform Action on it

Create Context Menu and perform action in Android

Android SQLite Database Tutorial with Example

No comments:

Post a Comment

Back to Top