Saturday, December 1, 2012

Enable infinite paging with Android ViewPager

Hi,

It's been a while since I blogged about something. This post is about enabling infinite paging with the Android ViewPager View. The question on how to do this was initially a question of mine on stackoverflow. Since some people asked me to publish some code, I thought I'd rather blog about this.

Note that this way has been worked for me. If you have any other way to do that, I'd be happy if you would share it.

In the stackoverflow thread I stated [1], that it is necessary to import the source files of the android v4 support library, because (at that time) there had to be a slight adjustment in the ViewPager Method. Now I found out that this is not necessary anymore (the adjustment was already there when I looked into the code. Looks like the android developers changed it).

So, all you need is to include the android support library to your project.
The result will look like this:

Page "-15"

Page "0"












Now let's get to the coding part. First of, we create a layout xml, which includes the ViewPager View.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >
    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

Then we create another xml for the page content. Let's call it content.xml.


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

For each page, we need a model class that holds the content of each page. We name the class PageModel. Note that we are also holding the view of the page (the TextView) that will be displayed to the user. This enables us to do the background content manipulation.

public class PageModel {

 private int index;
 private String text;
 public TextView textView;

 
 public PageModel(int index) {
  this.index = index;
  setIndex(index);
 }

 public int getIndex() {
  return index;
 }

 public void setIndex(int index) {
  this.index = index;
  setText(index);
 }

 public String getText() {
  return text;
 }

 private void setText(int index) {
  this.text = String.format("Page %s", index);
 }
}

Now we prepared the model for the paging. The following class manages the pages and the background content manipulation and switching the pages in a way that the user thinks that there are infinite pages.

Now we create an avitiy calles MainActivity:

public class MainActivity extends Activity {

 
 // we name the left, middle and right page
 private static final int PAGE_LEFT = 0;
 private static final int PAGE_MIDDLE = 1;
 private static final int PAGE_RIGHT = 2; 
 
 
 private LayoutInflater mInflater;
 private int mSelectedPageIndex = 1;
// we save each page in a model
 private PageModel[] mPageModel = new PageModel[3];
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  // initializing the model
  initPageModel();
  
  mInflater = getLayoutInflater();
  MyagerAdaper adapter = new MyagerAdaper();
    
  final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
  viewPager.setAdapter(adapter);
  // we dont want any smoothscroll. This enables us to switch the page
  // without the user notifiying this
  viewPager.setCurrentItem(PAGE_MIDDLE, false);
  
  viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
   
   @Override
   public void onPageSelected(int position) {
    mSelectedPageIndex = position;
   }
   
   @Override
   public void onPageScrolled(int arg0, float arg1, int arg2) { }
   
   @Override
   public void onPageScrollStateChanged(int state) {
    if (state == ViewPager.SCROLL_STATE_IDLE) {
     
     final PageModel leftPage = mPageModel[PAGE_LEFT];
     final PageModel middlePage = mPageModel[PAGE_MIDDLE];
     final PageModel rightPage = mPageModel[PAGE_RIGHT];
     
     final int oldLeftIndex = leftPage.getIndex();
     final int oldMiddleIndex = middlePage.getIndex();
     final int oldRightIndex = rightPage.getIndex();
     
     // user swiped to right direction --> left page
     if (mSelectedPageIndex == PAGE_LEFT) {
       
      // moving each page content one page to the right
      leftPage.setIndex(oldLeftIndex - 1);
      middlePage.setIndex(oldLeftIndex);
      rightPage.setIndex(oldMiddleIndex);
      
      setContent(PAGE_RIGHT);
      setContent(PAGE_MIDDLE);
      setContent(PAGE_LEFT);
      
     // user swiped to left direction --> right page
     } else if (mSelectedPageIndex == PAGE_RIGHT) {

      leftPage.setIndex(oldMiddleIndex);      
      middlePage.setIndex(oldRightIndex);
      rightPage.setIndex(oldRightIndex + 1);

      setContent(PAGE_LEFT);
      setContent(PAGE_MIDDLE);
      setContent(PAGE_RIGHT);
     }
     viewPager.setCurrentItem(PAGE_MIDDLE, false);
    }
   }
  });
 }
 
 private void setContent(int index) {
  final PageModel model =  mPageModel[index];
  model.textView.setText(model.getText());
 }

 private void initPageModel() {
  for (int i = 0; i < mPageModel.length; i++) {
   // initing the pagemodel with indexes of -1, 0 and 1
   mPageModel[i] = new PageModel(i - 1);
  }  
 }

 private class MyagerAdaper extends PagerAdapter {

  @Override
  public int getItemPosition(Object object) {
   return POSITION_NONE;
  }

  @Override
  public void destroyItem(ViewGroup container, int position, Object object) {
   container.removeView((View) object);
  }

  @Override
  public int getCount() {
   // we only need three pages
   return 3;
  }

  @Override
  public Object instantiateItem(ViewGroup container, int position) {
   TextView textView = (TextView)mInflater.inflate(R.layout.content, null);
   PageModel currentPage = mPageModel[position];
   currentPage.textView = textView;
   textView.setText(currentPage.getText());
   container.addView(textView);
   return textView;
  }

  @Override
  public boolean isViewFromObject(View view, Object obj) {
   return view == obj;
  }
 }
The "magic" for infinite paging happens at the onPageScrollStateChanged event. Each time when a page change occurs, we move the content to the right (or left depending on the page the user changed  to), change the content of each page (with the method setContent()) and switch again to the middle page. This enables the user to change the page (either to the left or right) again.

Of course the background content manipulation has been kept very simple. Moving more complex content from one page to another requires moving the childviews of a ViewGroup.

Hope it helped. And of course, the code can be downloaded here.

Happy coding

References
[1] Original stackoverflow question - http://stackoverflow.com/questions/7766630/changing-viewpager-to-enable-infinite-page-scrolling

Inspiring app that helped me writing the solution http://code.google.com/p/electricsleep/source/browse/trunk/src/com/androsz/electricsleepbeta/app/HistoryMonthActivity.java

Saturday, March 3, 2012

Extending interaction of views with gestures

Hi,
in this post i will talk about extending the interaction of an existing view with gestures. For example, you have a view that provides specific interaction like a ListView providing  you to select an element, or an EditText allowing you to enter text.
I will use an EditText widget and extend it with a swiping gesture to remove its content (screenshot below).

Okay, let's get started. First of all we create a simple
swipe-from-right-to-left gesture with the Gestures Builder (a good tutorial on how to add gestures in Android can be found here). We will call the gesture "clear_text".

After that is done, we will have an XML called "swipe_edit_text.xml" that describes the layout of our view.


<?xml version="1.0" encoding="utf-8"?>  
 <android.gesture.GestureOverlayView xmlns:android="http://schemas.android.com/apk/res/android"  
   android:id="@+id/gesture_overlay"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   android:eventsInterceptionEnabled="true"  
   android:uncertainGestureColor="@android:color/transparent" >  
   <EditText  
     android:id="@+id/edit_text"  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content" />  
 </android.gesture.GestureOverlayView>  

Important here is the line below to still allow the usual interaction of our view we are extending.
   android:eventsInterceptionEnabled="true"

Next we come to the code. Create a class SwipableEditText.
public class SwipableEditText extends FrameLayout {

 
 // the library to load the gestures
 private GestureLibrary mLibrary;

 // the gesture overlay
 private GestureOverlayView mGestureOverlay;
        // the view we are extending with gestures
 private EditText mEditText;
 
 public SwipableEditText(Context context) {
  this(context, null);
 }

 public SwipableEditText(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
 }

 public SwipableEditText(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  
  // inflate the layout
  final LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  FrameLayout li = (FrameLayout) inflater.inflate(R.layout.swipe_edit_text, this);
  mGestureOverlay = (GestureOverlayView) li.findViewById(R.id.gesture_overlay);
  mEditText = (EditText) li.findViewById(R.id.edit_text);
  
  // load the gestures
  mLibrary = GestureLibraries.fromRawResource(context, R.raw.gestures);
  if (!mLibrary.load())
   return;
  init();
 }
 
 private void init() {
  mGestureOverlay.addOnGesturePerformedListener(new GestureOverlayView.OnGesturePerformedListener() {
   

   @Override
   public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture) {

    ArrayList predictions = mLibrary.recognize(gesture);
    for (Prediction p : predictions) {
     // gesture accepted
     if (p.score > 1.0) {
       // user performed swipe gesture
       if (p.name.equalsIgnoreCase("clear_text")) {
        mEditText.setText("");
       }
      }
     }
    }
  });
 }
}

The swipe gesture performs the deletion of the text in the EditText Widget.
And that's it. Now you can integrate your view into any activity.
The code can be downloaded here.

Happy Coding.

Wednesday, August 24, 2011

QML in NotePad++

Here is a little file that enables syntax highlighting in the NotePadd++ editor for Windows.
Just copy the content into your APPDATA\Notepad++\userDefineLang.xml or simply create it if it isn't there. If you create it, make sure to firstly write
<NotepadPlus> </NotepadPlus>
and the content between those two brackets (please consider the case sensitivity).
Here's a little screenshot programming QML in Notepad++:

Monday, August 22, 2011

Handling Drag and Drop with different views using OnDragListener

Hello everyone,

it's been a while since the last time I blogged. The desktop summit was really interesting. I got to know new people and also saw some familiar faces (a desktop summit blog post should be on its way).
Anyways, this post is about something totally KDE unrelated. Since I am also an android developer I would like to give you some tips that helped me through little coding problems.

Here I will talk about  dragging and dropping. It is available since Android 3.0 (API version 10) and actually pretty straight forward. However I came across a little problem when using the
View.OnDragListener
interface. To make the problem description short:
every view that did not start the drag had to implement its own
View.OnDragListener.
All right less text, more code. Here is what works:
public class StuffActivity extends Activity implements OnDragListener {
   
    Button mDragView;
    Button mDropView;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mDragView = (Button)findViewById(R.id.dragger);
        mDragView.setOnDragListener(this);
        mDragView.setOnLongClickListener(new View.OnLongClickListener() {
      
            public boolean onLongClick(View v) {
                ClipData data = ClipData.newPlainText("text", mDragView.getText());
                v.startDrag(data, new MyDragShadowBuilder(mDragView), null, 0);
                return false;
           }
        });
        
        mDropView = (Button) findViewById(R.id.dropper);
        mDropView.setOnDragListener(new View.OnDragListener() {
   
   
    public boolean onDrag(View v, DragEvent event) {
        int action = event.getAction();
        switch(action) {
         case DragEvent.ACTION_DRAG_STARTED: {
          return true;
        }
        case DragEvent.ACTION_DRAG_LOCATION: {
          return true;
        }
        case DragEvent.ACTION_DROP: {
            // Gets the item containing the dragged data
            ClipData.Item item = event.getClipData().getItemAt(0);
            // insanity check
            if (item == null)
                return false;
            // Gets the text data from the item.
            CharSequence dragData = item.getText();
            mDropView.setText(dragData);

            return true;
        }
        default: {
            return false;
         }
        }
       }
      });
    }

    @Override
    public boolean onDrag(View v, DragEvent event) {
        int action = event.getAction();
        switch(action) {
        case DragEvent.ACTION_DRAG_STARTED: {
            Log.d(StuffActivity.class.getName(), "Starting from " + v.getClass().getName());
        return true;
    }
    case DragEvent.ACTION_DRAG_LOCATION: {
        return true;
    }
    case DragEvent.ACTION_DRAG_ENTERED: {
        Log.d(StuffActivity.class.getName(), "Entering " + v.getClass().getName());
        return true;
    }
    case DragEvent.ACTION_DROP: {
        v.invalidate();
        return true;
    }
    default: {
        return false;
    }
  }
 }

This activity has to Buttons. One is able to drag to the other Button. The other Button takes over the text of the first one.
This view implements the OnDragListener. As mentioned above there are two Buttons: mDragView and mDropView. Dragging and dropping for the mDragView field is handled by the implemented method
public boolean onDrag(View v, DragEvent event) { ... }
What is not working when you set:
mDropView.setOnDragListener(this);
instead of creating a new listener. Setting the listener to this does not contribute to the drag and drop event.

And that's it. Hope it helps. Thanks for reading.
The sourcecode can be downloaded here.

cheers

Please contact me if this post contains errors.

Thursday, June 30, 2011

Me too!

Just a quick note. Since everyone publishes his/her attendance at the desktop summit, I just wanted to say: Me too!




Some contributors of plasma-mediacenter will be there, too.
See you around.

Saturday, March 26, 2011

Advanced mediaservice and plugins. Part II: mediaservice

"Welcome the task that makes you go beyond yourself." (Frank Mcgee) [1]

...and also welcome back to this second part of Advanced mediaservice and plugins. In the first part, I introduced the the interface each plugin must implement and also showed how to write a provider plugin for the mediaservice. In this part I will show a little bit of what has been done so far and talk about further plans.
But first something I forgot to mention in the previous post.
Since we know now how to build the skeleton of our little plugin called MyProvider we still cannot tell what it should do. For that matter the mediaservice comes with some libraries that helps you query your provider more easily.

  • The file stdutils.js has some functions that makes it easy to do requests and build urls.
  • mediaapi.js contains the whole API for media and mediacollections. An error API was added for better error handling.

  • xmldom.js, xmlsax.js and xmlw3cdom.js are libraries that are able to parse xml documents. Since (right now) all of the plugins receive xml documents from their providers, these libraries are the ones parsing them. For more documentation about the xml libraries see [3].
If we pick up our example MyProvider again, the requesting and receiving information would look like this:


MyProvider.prototype.searchMedia = function(queryParams)
{
  // get the paramters from the service
  var text = queryParams['text'];
  var maxResults = queryParams['max-results'];
  var page = queryParams['page'];
  
  // build the url, result will be:
  //http://www.myprovider.com/search?text=...&per-page=...&page=..
  var url = buildUrl("http://www.myprovider.com/search",
    {
       "text" : text,
       "per-page" : maxResults,
       "page" : page
    });

  // we want to save the response
  var result = "";
  // make the request
  doRequest(engine, url, 
    function(job, data)
    {
       result += data.valueOf();
    },
    function(job)
   {
      // showing the result in the terminal
      print(result);
      // parse result here with the xml libraries
   }
  );
}
The two functions buildUrl() and doRequest() are included in the stdutils.js library. 

The code of the mediacenter is in [4]. Feel free to check out what we've been done so far. The code on the mediaservice is in dataengines/javascript/service/ and the plugins find themselves in dataengines/addons/.


Further plans
Right now, there hasn't been much activity in plasma-mediacenter-land and I hope that we continue more intensely on this project in the future.

However, a particular idea for this years Google summer of Code has been posted, which includes porting graphical elements to QML [2] and I am looking forward to see someone implementing it. A good way to make the mediaservice "useful" is to integrate it in that idea. I, myself was a little impatient to see something like that, so I implemented a little QML-plasmoid that connects to the mediaservice and queries a plugin (in that case it was flickr). Here is a little screenshot:


Thats all from here. Thank you for reading.

cheers
---
[1] http://www.quoteland.com/author/Frank-Mcgee-Quotes/6420/
[2] http://community.kde.org/GSoC/2011/Ideas#Project:_Plasma_media_center_first_release
[3] http://xmljs.sourceforge.net/
[4] http://projects.kde.org/projects/playground/multimedia/plasma-mediacenter

Thursday, March 24, 2011

Advanced mediaservice and plugins. Part I: plugins

Hello fellow blog readers!
It's been awhile the last time I wrote something. My last post was about my results of my google summer of code project Media and plugins, however since a couple of weeks, I continued on this project and would like to share my progress.
In this first part I would like give a little introduction on how to write a little provider plugin, in the second part I will talk about the achievements so far and further plans.

The Tutorial

Important here is, that a plugin must implement an interface, which has these four signatures:


searchMedia(text, max-results, page, media)

Makes a community search for media given a text. max-results, page and media are optional parameters. By default, max-results is 25, page is 1 and media depends on the media provider. E.g. in flickr, media has a default value of photos. In this case flickr is able to search for videos and photos. On the other hand, picasa does not have this ability.


searchCollection(text)

Makes a community search for media collections given a text. Some providers do not provide the functionality to search for collections. In that case this function can be left blank.


getUploadedUserMedia(user, max-results, page)

Receives all uploaded media from a user. max-results and page are optional parameters. Again, by default max-results has the value  25 and page the value 1.

getUploadedUserCollection(user,max-results, page)

Receives all uploaded collections of media from a user. Also here max-results and page are optional parameters with the above mentioned default values.

With that information about the interface signature we are now able to write our own plugin, which looks like this:

// the constructor
function MyProvider()
{
   this.name = "myProvider";
}
MyProvider.prototype.searchMedia(queryParams){
   print("Searching for media " + queryParams['text']);
}

MyProvider.prototype.searchCollection(queryParams){
   print("Searching for collections " + queryParams['text']);
}

MyProvider.prototype.getUploadedUserMedia(queryParams){
 print("Getting uploaded media from user " + queryParams['user']);
}
 
MyProvider.prototype.getUploadedUserCollection(queryParams){
   print("Getting uploaded collections from user " + queryParams['user']);
}

Note that in JavaScript we use prototypes to access information across function scopes.

The parameter in each function queryParams is an array, because we receive  the parameters from the mediaservice and we access them with e.g. queryParams['text'] if we want to get the text parameter.
The 'text' and 'user' parameters are mandatory in each function (if  provided). Without them every function will not able to perform any operation.

At the end of each plugin, we have to "tell" the plasmaEngine, that this code above is a plugin. We do that with

registerAddon(MyProvider);
And that's about it. Thank you for reading. The second part is on its way. 

Stay tuned...