Using Contextual Action Bar as Selection Toolbar

In Open Event Android Organizer App we were using Android Action Bar as Toolbar to allow deleting and editing of the list items but this implementation was buggy and not upto the industry standards. So we decided to implement this using Contextual Action Bar as a selection toolbar.

Specifications

We are using MVP Architecture so there will be Fragment class to interact with the UI and Presenter class to handle all the business logic and interact with network layer.

The SessionsFragment is the Fragment class for displaying List of Sessions to the user. We can long press any item to select it, entering into contextual action bar mode. Using this we will be able to select multiple items from list by a click and delete/edit them from toolbar.

To enter in Contextual Action Bar Mode use

ActionMode actionMode = getActivity().startActionMode(actionCallback);

To exit Contextual Action Bar Mode use

if (actionMode != null)
actionMode.finish();

We will implement Action.Callback interface in out fragment class. It’s contains three method declarations –

  1. onCreateActionMode – This method is executed when the contextual action bar is  created. We will inflate the toolbar menu using MenuInflator and set new status bar color.
  2. onPrepareActionMode – This method is executed after onCreateActionMode and also whenever the Contextual Action Bar is invalidated so we will set the visibility of delete button in toolbar here and return true to ignify that we have made some changes.
  3. onActionItemClicked – This method is executed whenever a menu item in toolbar is clicked. We will call the function showDeleteDialog.
  4. onDestroyActionMode – This method is executed whenever user leaves the contextual action bar mode by pressing back button on toolbar or back button in keyboard etc. Here we will make unselect all the selected list items and the status bar color.
public ActionMode.Callback actionCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.menu_sessions, menu);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
statusBarColor = getActivity().getWindow().getStatusBarColor();
getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.color_top_surface));
}
return true;
}@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
MenuItem menuItemDelete = menu.findItem(R.id.del);
MenuItem menuItemEdit = menu.findItem(R.id.edit);
menuItemEdit.setVisible(editMode);
menuItemDelete.setVisible(deletingMode);
return true;
}

@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.del:
showDeleteDialog();
break;
case R.id.edit:
getPresenter().updateSession();
break;
default:
return false;
}
return false;
}

@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
getPresenter().resetToolbarToDefaultState();
getPresenter().unselectSessionList();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//return to “old” color of status bar
getActivity().getWindow().setStatusBarColor(statusBarColor);
}
}
};

The showDeleteDialog method shows a AlertDialog to the user before deleting the selected items in the list. User can click on Ok to delete the items or on Cancel to close the dialog.

public void showDeleteDialog() {
if (deleteDialog == null)
deleteDialog = new AlertDialog.Builder(context)
.setTitle(R.string.delete)
.setMessage(String.format(getString(R.string.delete_confirmation_message),
getString(R.string.session)))
.setPositiveButton(R.string.ok, (dialog, which) -> {
getPresenter().deleteSelectedSessions();
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {
dialog.dismiss();
})
.create();deleteDialog.show();
}

When user presses Ok then we call the method deleteSelectedSessions in presenter to delete the sessions.

Resources

  1. Offical doumentation for Contextual Action Bar by Google https://developer.android.com/guide/topics/ui/menus#CAB
  2. Official Documentation for Alert Dialog by Google https://developer.android.com/guide/topics/ui/dialogs
  3. Codebase for Open Event OrganIzer App on Github https://github.com/fossasia/open-event-orga-app/

Implementing the List View of the Skill Cards

In this blog post, we are going to understand the implementation of the UI for the SUSI.AI skill card that is displayed on various routes of the SUSI Skill CMS Web-App. Now, there are two types of views of the views for the skill cards – List view and Grid view. We will learn to implement the List View in this blog.

Final UI of the Skill Card

Going through the implementation

The UI has multiple components –

  • The image thumbnail.
  • The title and author section,
  • Below that we have examples, ratings and the description section.

Fetching the data

  • The Skill Metadata for each skill is passed as props from the parent of the component, where this UI is implemented. This data object contains the various data points that are needed to display the UI. The key values used are –
    • skill_name – Used in the Title of the Skill Card
    • image – Used to display the thumbnail image of the skill
    • model – used to create the link to the Skill Details page
    • group – used to create the link to the Skill Details page
    • language – used to create the link to the Skill Details page
    • skill_tag – used to create the link to the Skill Details page
    • examples – used to display the examples card.
    • author – used to display the Author name
    • skill_rating – Used to display the stars and the total number of ratings of the skill
  • The following image shows the various areas, where the data is being used.

Parsing the data and creating JSX

  • Below is the code used to parse the data and achieving the UI, followed by the explanation.

…..
loadSkillCards = () => {
  let cards = [];
  Object.keys(this.state.skills).forEach(el => {
    let skill = this.state.skills[el];
    let skill_name = 'Name not available', examples = [], image = '', description =      
    'No description available', author_name = 'Author', average_rating = 0, 
    total_rating = 0;
    if (skill.skill_name) 
      skill_name = skill.skill_name.charAt(0).toUpperCase() + skill_name.slice(1);
    ….
    // Similarly parse, image, descriptions, author 
    ….
    if (skill.examples)
      examples = skill.examples.slice(0, 2); // Select max 2 examples
    if (skill.skill_rating) {
      average_rating = parseFloat(skill.skill_rating.stars.avg_star);
      total_rating = parseInt(skill.skill_rating.stars.total_star, 10);
    }
    cards.push(
      <div style={styles.skillCard} key={el}>
        <div style={styles.imageContainer}>
          // Display the image, else default avatar compoennt CircleImage
        </div>
        <div style={styles.content}>
          <div style={styles.header}>
            // Add Link to the skill title
              <div style={styles.title}><span>{skill_name}</span></div>
            <div style={styles.authorName}><span>{author_name}</span></div>
          </div>
          <div style={styles.details}>
            <div style={styles.exampleSection}>
              {examples.map((eg, index) => { return (
                <div key={index} style={styles.example}>&quot;{eg}&quot;</div>);
              })}
            </div>
            <div style={styles.textData}>
              <div style={styles.row}>
                <div style={styles.rating}>
                  // Show the 5-star rating section
                </div>
              </div>
              <div style={styles.row}>
                // Insert the skill description
              </div>
        //Close the div tags
    );
  });
  this.setState({cards});
};

render() {
.
.
  return (<div style={styles.gridList}>{skillDisplay}</div>);
}
.
.

 

  • An array of skills is passed as props and set in the state of the component in the constructor lifecycle method. The loadSkillCards() function is called in the didComponentMount lifecycle method, which is responsible for creating the JSX for all the Skill Cards.
  • In this function, the map property of array is used, to iterate over each skill and the corresponding Skill Card is pushed to the cards array, after successfully parsing the data.
  • At the end of the function definition, the state is updated, which in turn triggers the render function. In the render function, the cards are returned enclosed in a <div> tag. This helps us to create the above UI.

Styling the UI

Some important styling used is shown below. For the full styles object, please follow this link.

const styles = {
  skillCard: {
    width: '100%',
    overflow: 'hidden',
    display: 'flex',
    flexDirection: 'row',
    borderTop: '1px solid #eaeded',
    padding: 7,
  },
  imageContainer: {
    display: 'inline-block',
    alignItems: 'center',
    padding: '10px',
    background: '#fff',
    height: '218px',
    marginBottom: '6px',
  },
  image: {
    position: 'relative',
    height: '180px',
    width: '180px',
    verticalAlign: 'top',
    borderRadius: '50%',
  },
….
  gridlist: {
    marginTop: '20px',
    marginBottom: '40px',
    padding: '0px 10px',
    width: '100%',
….
  example: {
    fontStyle: 'italic',
    fontSize: '14px',
    padding: '14px 18px',
    borderRadius: '4px',
    border: '1px #ddd solid',
    float: 'left',
    display: 'flex row',
    justifyContent: 'center',
    alignItems: 'center',
    width: 192,
  },
….
};

export default styles;

 

I hope the implementation of the UI is clear and proved to be helpful for your understanding.

Resources

Showcase of the Material-UI icons (used for View type icons) – https://material.io/icons/

Adding Functionality to Switch between List and Grid View of the Skill Cards on SUSI.AI CMS

In this blog post, we are going to understand the implementation of the functionality that enables the user to switch between the List View and the Grid View UI for the skill cards that is displayed on various routes of the SUSI Skill CMS Web-App. Let us go through the implementation in the blog –

Working of the feature

Going through the implementation

  • The UI for implementing the switching of views was achieved via the use of RadioButtonGroup component of the Material-UI library for React.
  • The type of view currently being shown was stored in the component state of the BrowseSkill component as viewType, whose default value is set to list, indicating that the skills are firstly shown in a List View.

.
.

export default class BrowseSkill extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        .
        …..
        viewType: 'list',
        ….
    };
  }
….
}

 

  • The RadioButtonGroup component has 2 child components, for each view. The child component that is to be used is RadioButton.
  • The props passed in the RadioButtonGroup are –
    • name : It is the name given to the component.
    • defaultSelected : It is the default view type.
    • style : It contains the style object of the UI.
    • valueSelected : It is set to the state variable assigned for storing view type.
    • onChange : It is the handler which executes, when the radio buttons are clicked.
  • The style for the desktop view and mobile view is different depending on the screen size and is follows –

//Mobile view
Style {
    right: 12,
    position: 'absolute',
    top: 216,
    display: 'flex',
}

//Desktop view
style={
    display: 'flex',
    marginTop: 34
}

 

  • The props passed in the RadioButton are –
    • value : The value stored in the state, that is responsible for the view type. The values for List and Grid view are list and grid respectively.
    • label : The label for the RadioButton.
    • labelStyle : The style object for the label.
    • checkedIcon : The icon used in the checked state.
    • uncheckedIcon : The icon used in the unchecked state.

UI of the Radio Buttons

  • The onClick handler of the radio buttons is –

handleViewChange = (event, value) => {
    this.setState({ viewType: value });
};

 

  • The code snippet for the UI implementation, written inside the render function is as follows :

….
<RadioButtonGroup
  name="view_type"
  defaultSelected="list"
  style={
    window.innerWidth < 430
      ? {
          right: 12,
          position: 'absolute',
          top: 216,
          display: 'flex',
        }
      : { display: 'flex', marginTop: 34 }
  }
  valueSelected={this.state.viewType}
  onChange={this.handleViewChange}
>
  <RadioButton
    value="list"
    label="List view"
    labelStyle={{ display: 'none' }}
    style={{ width: 'fit-content' }}
    checkedIcon={
      <ActionViewStream style={{ fill: '#4285f4' }} />
    }
    uncheckedIcon={<ActionViewStream />}
  />
  <RadioButton
    value="grid"
    label="Grid view"
    labelStyle={{ display: 'none' }}
    style={{ width: 'fit-content' }}
    checkedIcon={
      <ActionViewModule style={{ fill: '#4285f4' }} />
    }
    uncheckedIcon={<ActionViewModule />}
  />
</RadioButtonGroup>
….

 

I hope the implementation of the switching between Views would be clear after going through the blog and proved to be helpful for your understanding.

References

Enhancing Rotation in Phimp.me using Horizontal Wheel View

Installation

To implement rotation of an image in Phimp.me,  we have implemented Horizontal Wheel View feature. It is a custom view for user input that models horizontal wheel controller. How did we include this feature using jitpack.io?

Step 1: 

The jitpack.io repository has to be added to the root build.gradle:

allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}


Then, add the dependency to your module build.gradle:

compile 'com.github.shchurov:horizontalwheelview:0.9.5'


Sync the Gradle files to complete the installation.

Step 2: Setting Up the Layout

Horizontal Wheel View has to be added to the XML layout file as shown below:

<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2">

<com.github.shchurov.horizontalwheelview.HorizontalWheelView
android:id="@+id/horizontalWheelView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toStartOf="@+id/rotate_apply"
android:padding="5dp"
app:activeColor="@color/accent_green"
app:normalColor="@color/black" />

</FrameLayout>


It has to be wrapped inside a Frame Layout to give weight to the view.
To display the angle by which the image has been rotated, a simple text view has to be added just above it.

<TextView
android:id="@+id/tvAngle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:textColor="@color/black"
android:textSize="14sp" />

Step 3: Updating the UI

First, declare and initialise objects of HorizontalWheelView and TextView.

HorizontalWheelView horizontalWheelView = (HorizontalWheelView) findViewById(R.id.horizontalWheelView);
TextView tvAngle= (TextView) findViewById(R.id.tvAngle);

 

Second, set up listener on the HorizontalWheelView and update the UI accordingly.

horizontalWheelView.setListener(new HorizontalWheelView.Listener() {
@Override
public void onRotationChanged(double radians) {
updateText();
updateImage();
}
});


updateText()
updates the angle and updateImage() updates the image to be rotated. The following functions have been defined below:

private void updateText() {
String text = String.format(Locale.US, "%.0f°", horizontalWheelView.getDegreesAngle());
tvAngle.setText(text);
}

private void updateImage() {
int angle = (int) horizontalWheelView.getDegreesAngle();
//Code to rotate the image using the variable 'angle'
rotatePanel.rotateImage(angle);
}


rotateImage()
is a method of ‘rotatePanel’ which is an object of RotateImageView, a custom view to rotate the image.

Let us have a look at some part of the code inside RotateImageView.

private int rotateAngle;


‘rotateAngle’ is a global variable to hold the angle by which image has to be rotated.

public void rotateImage(int angle) {
rotateAngle = angle;
this.invalidate();
}


The method invalidate() is used to trigger UI refresh and every time UI is refreshed, the draw() method is called.
We have to override the draw() method and write the main code to rotate the image in it.

The draw() method is defined below:

@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (bitmap == null)
return;
maxRect.set(0, 0, getWidth(), getHeight());// The maximum bounding rectangle

calculateWrapBox();
scale = 1;
if (wrapRect.width() > getWidth()) {
scale = getWidth() / wrapRect.width();
}

canvas.save();
canvas.scale(scale, scale, canvas.getWidth() >> 1,
canvas.getHeight() >> 1);
canvas.drawRect(wrapRect, bottomPaint);
canvas.rotate(rotateAngle, canvas.getWidth() >> 1,
canvas.getHeight() >> 1);
canvas.drawBitmap(bitmap, srcRect, dstRect, null);
canvas.restore();
}

private void calculateWrapBox() {
wrapRect.set(dstRect);
matrix.reset(); // Reset matrix is ​​a unit matrix
int centerX = getWidth() >> 1;
int centerY = getHeight() >> 1;
matrix.postRotate(rotateAngle, centerX, centerY); // After the rotation angle
matrix.mapRect(wrapRect);
}

 

And here you go:

Resources

Refer to Github- Horizontal Wheel View for more functions and for a sample application.

Creating a Notification in Open Event Android App

It is a good practice to show user a notification for alerts and have their attention for important events they want to remember. Open Event Android app shows notifications for the actions like bookmarks, upcoming events etc. In this blog we learn how to create similar kind of alert notification.

 

Displaying notification after bookmarking a track

NotificationCompat is available as part of the Android Support Library, so the first step is opening your project’s module-level build.gradle file and adding the support library to the dependencies section. First we initialize the notification manager with the context of application so a user can see notification irrespective of where it is in app.

NotificationManager mManager = (NotificationManager) this.getApplicationContext().getSystemService(NOTIFICATION_SERVICE);
int id = intent.getIntExtra(ConstantStrings.SESSION, 0);
String session_date;
Session session = realmRepo.getSessionSync(id);

We then get the info we want to display in the notification from the intent. While adding an action to your notification is optional, the reality is that the vast majority of applications add actions to their notifications. We define a notification action using a PendingIntent. In this instance, we update our basic notification with a PendingIntent.

Intent intent1 = new Intent(this.getApplicationContext(), SessionDetailActivity.class);
intent1.putExtra(ConstantStrings.SESSION, session.getTitle());
intent1.putExtra(ConstantStrings.ID, session.getId());
intent1.putExtra(ConstantStrings.TRACK,session.getTrack().getName());
PendingIntent pendingNotificationIntent = PendingIntent.getActivity(this.getApplicationContext(), 0, intent1, PendingIntent.FLAG_UPDATE_CURRENT);
Bitmap largeIcon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);

We also test the condition for the OS version to display the marker image, see image 1 for reference. The minimum requirement for a notification are:

  • An icon: Create the image you want to use and then add it to you project’s ‘drawable’ folder. Here notification shows bookmark option
  • Title text. You can set a notification’s title either by referencing a string resource, or by adding the text to your notification directly.
  • Detail text. This is the most important part of your notification, so this text must include everything the user needs to understand exactly what they’re being notified about.
int smallIcon = R.drawable.ic_bookmark_white_24dp;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) smallIcon = R.drawable.ic_noti_bookmark;

String session_timings = String.format("%s - %s",
       DateConverter.formatDateWithDefault(DateConverter.FORMAT_12H, session.getStartsAt()),
       DateConverter.formatDateWithDefault(DateConverter.FORMAT_12H, session.getEndsAt()));
session_date = DateConverter.formatDateWithDefault(DateConverter.FORMAT_DATE_COMPLETE, session.getStartsAt());

Finally we build notification using notification builder having various options to set text style, small icons, big icon etc., see the complete class here,

NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this)
       .setSmallIcon(smallIcon)
       .setLargeIcon(largeIcon)
       .setContentTitle(session.getTitle())
       .setContentText(session_date + "\n" + session_timings)
       .setAutoCancel(true)
       .setStyle(new NotificationCompat.BigTextStyle().bigText(session_date + "\n" + session_timings))
       .setContentIntent(pendingNotificationIntent);
intent1.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);

mBuilder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION));
mManager.notify(session.getId(), mBuilder.build());

References