Android Tutorial How I wrapped a HTML5, JavaScript & Css based Library...

Mashiane

Expert
Licensed User
Hi there..

Eye Candy

To be able to do this, you need a good library that you want to wrap. A library like this will run inside a webview as its purely javascript, so some knowledge of HTML5 at most and some css will be recommended. I'm still learning both through trial and error.

Secondly, I wanted to trap the events that happen within my components, so I used webview extras for that.

Thirdly, I wanted to have the library include internaly the javascript and css files that I'm using. Please note the respective licenses for that. I decided on some community based libraries.

Steps

1. Create a project in b4a and insert the normal library attributes... Here is an example. This should be in the Main activity

B4X:
#Region  Project Attributes
    #LibraryName: AMTileView108
    'SupportedOrientations possible values: unspecified, landscape or portrait.
    #SupportedOrientations: unspecified
    #CanInstallToExternalStorage: false
    #LibraryAuthor: Anele Mbanga - anele@mbangas.com
    #LibraryVersion: 1.08
#End Region
2. My library is based on the premise that it will run inside a webview, so I needed to create a page where all components will sit. This I created a page class to be a holder using HTML5 methodology. This page will parent the other components, will fire javascript code using WebViewExtras etc and will get and set components attributes during run time.

As my page will fire events, I created some events and also some design properties (this I will test later on). This I did by adding a CustomView class and updating the events...

B4X:
#Event: TileClicked (value As String)
#Event: TabClicked (value As String)
#Event: MenuClicked (value As String, childIndex as string)
#Event: GroupButtonClick (value As String)
#Event: RadialSliderChange (element as String, Value as string)
#Event: RangeSliderChange (element as String, Value as string)
#Event: RatingChange (element as string, value as string)
#Event: Loaded ()
#Event: RangeSliderRead(value1 as string, value2 as string)
3. I then defined some global variables I will use and also the interface with the webview extras library on this page class...

B4X:
Sub Class_Globals
    Public ID As String
    Private jsFiles As List
    Private cssFiles As List
    Public PageName As String
    Public Title As String
    Public Description As String
    Public Author As String
    Public Minify As Boolean
    Private EventName As String 'ignore
    Private CallBack As Object 'ignore
    Private mBase As Panel
    Private webTile As WebView
    Dim we As WebViewExtras
    Dim client As DefaultWebChromeClient
    Dim jsi As DefaultJavascriptInterface
    Type RenderModeEnum(windows As String, auto As String, ios7 As String, android As String, flat As String)
    Public EnumRenderMode As RenderModeEnum
    Public FontSize As String
    Public FontFamily As String
    Public BackgroundImage As String
    Public BackgroundColor As String
    Public jfl As JarFileLoader
    Public ShowProgress As Boolean
    Public ProgressMessage As String
    Public DebugMode As Boolean
    Private pageDiv As HTMLElement
    Public HasScrollPanel As Boolean
    Private sv As HTMLElement
    Type OrientationEnum(vertical As String, horizontal As String)
    Public EnumOrientation As OrientationEnum
    Type PrecisionEnum(Full As String, Half As String, Exact As String)
    Public EnumPrecision As PrecisionEnum
    Type ShapeEnum(star As String, circle As String, diamond As String, heart As String, pentagon As String, square As String, triangle As String)
    Public EnumShape As ShapeEnum
    Type PagerPositionEnum(top As String, bottom As String, left As String, right As String)
    Public EnumPagerPosition As PagerPositionEnum
    Public RenderMode As String
    Public ShowScrollBars As Boolean
    Type itemStyleEnum(bothblock As String, image As String)
    Public EnumItemStyle As itemStyleEnum
    Type IconsEnum(iconadd As String, iconback As String, iconbookmark As String, iconclose As String, iconcompose As String, iconcopy As String, iconcut As String, _
    icondelete As String, icondone As String, iconedit As String, iconmail As String, iconnext As String, iconrefresh As String, iconoverflow As String, _
    iconpaste As String, iconreply As String, iconsave As String, iconsearch As String, iconsettings As String, iconshare As String)
    Public EnumIcons As IconsEnum
End Sub
From above, the most important crucial things are the webview (we will render our components in it), the JarFileLoader (this extracts js and css files from the compiled .jar file), the WebViewExtras, WebChromeClient and JSInterface), without these, this library would not work at all.

The output of the final library for me has been the Eye Candy above, all purely javascript and some css.
 

Attachments

Last edited:

Mashiane

Expert
Licensed User
4. I wanted my library to be able to be added to an activity and also to a panel, so I defined two methods to achieve this, we are still in the same AMPage class.

B4X:
'add control to activity   
Public Sub AddToActivity(Parent As Activity, Left As Int, Top As Int, Width As Int, Height As Int)
    Parent.AddView(mBase, Left, Top, Width, Height)
End Sub

'add control to panel
Public Sub AddToPanel(Parent As Panel, Left As Int, Top As Int, Width As Int, Height As Int)
    Parent.AddView(mBase, Left, Top, Width, Height)
End Sub
   
'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize(vCallback As Object, vEventName As String)
    EventName = vEventName
    CallBack = vCallback
    Init
End Sub
 

Mashiane

Expert
Licensed User
5. The Init method from the above Initialize methods, does the basic setups of the environment. This will extracts the necessary needed files and also I add the methods that will be executed via javascript but called within my b4a app using webviewextras.


B4X:
Private Sub Init
    EnumIcons.Initialize
    EnumIcons.iconadd = "Add"
    EnumIcons.iconback = "Back"
    EnumIcons.iconbookmark = "Bookmark"
    EnumIcons.iconclose = "Close"
    EnumIcons.iconcompose = "Compose"
    EnumIcons.iconcopy = "Copy"
    EnumIcons.iconcut = "Cut"
    EnumIcons.icondelete = "Delete"
    EnumIcons.icondone = "Done"
    EnumIcons.iconedit = "Edit"
    EnumIcons.iconmail = "Mail"
    EnumIcons.iconnext = "Next"
    EnumIcons.iconrefresh = "Refresh"
    EnumIcons.iconoverflow = "Overflow"
    EnumIcons.iconpaste = "Paste"
    EnumIcons.iconreply = "Reply"
    EnumIcons.iconsave = "Save"
    EnumIcons.iconsearch = "Search"
    EnumIcons.iconsettings = "Settings"
    EnumIcons.iconshare = "Share"
 
    EnumItemStyle.Initialize
    EnumItemStyle.bothblock = "bothblock"
    EnumItemStyle.image = "image"
    ShowScrollBars = True
    RenderMode = "ios7"
    EnumPagerPosition.Initialize
    EnumPagerPosition.bottom = "bottom"
    EnumPagerPosition.left = "left"
    EnumPagerPosition.right = "right"
    EnumPagerPosition.top = "top"
    EnumShape.Initialize
    EnumShape.star = "star"
    EnumShape.circle = "circle"
    EnumShape.diamond = "diamond"
    EnumShape.heart = "heart"
    EnumShape.pentagon = "pentagon"
    EnumShape.square = "square"
    EnumShape.triangle = "triangle"
    EnumPrecision.Initialize
    EnumPrecision.Exact = "exact"
    EnumPrecision.Full = "full"
    EnumPrecision.Half = "half"
    EnumOrientation.Initialize
    EnumOrientation.vertical = "vertical"
    EnumOrientation.horizontal = "horizontal"
    pageDiv.Initialize("page", "div")
    pageDiv.AddAttribute("data-role", "appview")
    pageDiv.AddClass(ID & "bg")
    'init the scrollview
    sv.Initialize("scroller","div")
    sv.AddAttribute("data-role","ejmscrollpanel")
    sv.AddAttribute("data-ej-target", "page")
    webTile.Initialize("webTile")
    mBase.Initialize("mBase")
    webTile.JavaScriptEnabled = True
    webTile.ZoomEnabled = False
    we.Initialize(webTile)
    jsi.Initialize
    we.addJavascriptInterface(jsi, "B4A")
    we.JavaScriptEnabled = True
    client.Initialize("wcc")
    we.SetWebChromeClient(client)
    mBase.AddView(webTile,0dip,0dip,100%x,100%y)
    ShowProgress = True
    ProgressMessage = "Working on it, please wait..."
    'extract the library contents
    jfl.Initialize(False)
    'jfl.AssetFromJar("bootstrap.min.css")
    jfl.AssetFromJar("ej.mobile.all.min.css")
    'jfl.assetfromjar("angular.min.js")
    'jfl.AssetFromJar("bootstrap.min.js")
    'jfl.AssetFromJar("ej.globalize.min.js")
    jfl.AssetFromJar("ej.mobile.all.min.js")
    jfl.AssetFromJar("ej.unobtrusive.min.js")
    'jfl.AssetFromJar("ej.widget.angular.min.js")
    'jfl.assetfromjar("ej.widget.ko.min.js")
    'jfl.AssetFromJar("excanvas.min.js")
    jfl.AssetFromJar("jquery.easing.1.3.min.js")
    jfl.AssetFromJar("jquery-3.1.1.min.js")
    jfl.AssetFromJar("jsrender.min.js")
    'jfl.AssetFromJar("knockout.min.js")
 
    EnumRenderMode.Initialize
    EnumRenderMode.windows = "windows"
    EnumRenderMode.auto = "auto"
    EnumRenderMode.android = "android"
    EnumRenderMode.flat = "flat"
    EnumRenderMode.ios7 = "ios7"
    Title = "page"
    cssFiles.Initialize
    jsFiles.Initialize
    Description = ""
    Author = ""
    Minify = False
    LoadSyncfusionFramework
    AddJSCode("function menuclicked(e) {B4A.CallSub('menuclicked', false, e.index, e.childIndex);}")
    AddJSCode("function groupbuttonclick(args) {B4A.CallSub('groupbuttonclick', false, args.text);}")
    AddJSCode("function menucontrolclicked(args) {B4A.CallSub('menucontrolclicked', false, args.text);}")
    AddJSCode("function tabclick(args) {B4A.CallSub('tabclick', false, args.id);}")
    AddJSCode("function tileclicked(args) { var id = this.element[0].id; B4A.CallSub('tileclicked', false, id);}")
    'debug the page
    'Dim WVJO As JavaObject = webTile
    'WVJO.RunMethod("setWebContentsDebuggingEnabled",Array(True))
    ' open browser in chrome://inspect/#devices
    ' see here https://developers.google.com/web/tools/chrome-devtools/remote-debugging/?hl=en
    FontSize = ""
    BackgroundColor = ""
    HasScrollPanel = True
End Sub
As you might have noted, my library is based on Synfusion controls, thus my page structure meets that definition. The AddJSCode method will add those javascript pieces to the page definition I'm trying to create here.

The purpose of this is to have the skeleton HTML content of the page to put the components in. I will discuss the jfl methods later on.
 
Last edited:

Mashiane

Expert
Licensed User
6. Then I linked the javascript code to the code that should be called linked to the b4a events..

This line above, speaks to this code, thus, when the menucontrolclicked javascript is called when a user executes a command, then b4a MenuControlClicked event will be fired. If you did see the RadialMenu, each time you clicked a menu item, the Activity.Title, changed, this is how this was achieved, by linking javascript to b4a using WebViewExtras. Powerful Library that is.

AddJSCode("function menucontrolclicked(args) {B4A.CallSub('menucontrolclicked', false, args.text);}")

B4X:
'radial slider change
private Sub menucontrolclicked(value As String)
    If SubExists(CallBack, EventName & "_MenuControlClicked") Then
        CallSubDelayed2(CallBack,EventName & "_MenuControlClicked", value)
    End If
End Sub


'radial slider change
private Sub tabclick(element As String, value As String)
    If SubExists(CallBack, EventName & "_TabClicked") Then
        CallSubDelayed3(CallBack,EventName & "_TabClicked", element, value)
    End If
End Sub

'radial slider change
private Sub rangesliderchange(element As String, value As String)
    If SubExists(CallBack, EventName & "_RangeSliderChange") Then
        CallSubDelayed3(CallBack,EventName & "_RangeSliderChange", element, value)
    End If
End Sub

'radial slider change
private Sub radialsliderchange(element As String, value As String)
    If SubExists(CallBack, EventName & "_RadialSliderChange") Then
        CallSubDelayed3(CallBack,EventName & "_RadialSliderChange", element, value)
    End If
End Sub

'rating changed
private Sub ratingchange(element As String, value As String)
    If SubExists(CallBack, EventName & "_RatingChange") Then
        CallSubDelayed3(CallBack,EventName & "_RatingChange", element, value)
    End If
End Sub

'run when a tile is clicked
private Sub tileclicked(value As String)
    If SubExists(CallBack, EventName & "_TileClicked") Then
        CallSubDelayed2(CallBack,EventName & "_TileClicked", value)
    End If
End Sub

'run when a tile is clicked
private Sub groupbuttonclick(value As String)
    If SubExists(CallBack, EventName & "_GroupButtonClick") Then
        CallSubDelayed2(CallBack,EventName & "_GroupButtonClick", value)
    End If
End Sub

'run when a tile is clicked
private Sub menuclicked(value As String, childIndex As String)
    If SubExists(CallBack, EventName & "_MenuClicked") Then
        CallSubDelayed3(CallBack,EventName & "_MenuClicked", value, childIndex)
    End If
End Sub
 

Mashiane

Expert
Licensed User
7. The AMPage HTML

B4X:
'return the html content in html
Public Sub HTML() As String
    If FontSize.Length > 0 Or FontFamily.Length > 0 Then
        Dim css As CssElement
        css.Initialize("body")
        If FontSize.Length > 0 Then css.AddAttribute("font-size", FontSize & "px")
        If FontFamily.Length > 0 Then  css.AddAttribute("font-family", FontFamily)
        AddStyle(css.HTML)
    End If
    Dim sb As StringBuilder
    sb.Initialize
    Dim sm As String = $"<!DOCTYPE html>
    <html lang="en">
    <head>
    <meta name="msapplication-tap-highlight" content="no" />
    <meta id="viewport" name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no" />
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="format-detection" content="telephone=no">
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="description" content="<<description>>" />
    <meta name="author" content="<<author>>" />
    <title><<title>></title>"$
    sb.Append(sm)
    'add the css files
    For Each strCSS As String In cssFiles
        sb.Append(AddLink(strCSS))
    Next
    'add javascript files
    For Each strJS As String In jsFiles
        sb.Append(AddJS(strJS))
    Next
    sb.Append(CRLF)
    sb.append("</head>").Append(CRLF)
    sb.Append("<body>").Append(CRLF)
    sb.Append(pageDiv.GetStyles)
    sb.Append(pageDiv.HTML)
    If HasScrollPanel = True Then
        sv.AddAttribute("data-ej-rendermode", RenderMode)
        If ShowScrollBars = False Then sv.AddAttribute("data-ej-showscrollbars", False)
        sb.Append(sv.HTML)
    End If
    sb.Append(pageDiv.GetJavaScript)
    sb.Append("</body>").Append(CRLF)
    sb.Append("</html>").Append(CRLF)
    Dim strOut As String = sb.ToString
    strOut = strOut.Replace("<<description>>", Description)
    strOut = strOut.Replace("<<author>>", Author)
    strOut = strOut.Replace("<<title>>", Title)
    strOut = strOut.trim
    If Minify = True Then
        Return strOut.Replace(CRLF,"")
    Else
        Return strOut
    End If
End Sub
This code here will build the page HTML structure including everything we need. You also need to just achieve that point, by ensuring that your page HTML code is correct to be able to be discplayed in the webview.
 

Mashiane

Expert
Licensed User
8. The next step was then to build each of the elements that I want to be displayed in the page depending on my needs.

Below is the code for the AMButtonGroup component of the AMTileView library.

B4X:
Sub Class_Globals
    Public ID As String
    Private page As AMPage
    Private gb As HTMLElement
    Private gbh As HTMLElement
    Public RenderMode As String
    Public MultipleSelect As Boolean
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize(pg As AMPage, sID As String)
    MultipleSelect = False
    ID = sID
    page = pg
    gbh.Initialize(sID & "sample","div")
    gbh.AddClass(sID & "sample")

    Dim css As CssElement
    css.Initialize("." & sID & "sample")
    css.AddAttribute("padding-top", "20px")
    css.AddAttribute("text-align", "center")
    'center the group button
    css.AddAttribute("display", "table")
    css.AddAttribute("margin", "0 auto")
    page.AddStyle(css.HTML)
  
    gb.Initialize(sID,"div")
    gb.AddAttribute("data-role", "ejmgroupbutton")
    gb.addattribute("data-ej-selecteditemindex","0")
    gb.AddAttribute("data-ej-touchend", "groupbuttonclick")
    RenderMode = pg.EnumRenderMode.ios7
End Sub

'add a style to the element
Sub AddStyle(value As String)
    page.AddStyle(value)
End Sub

'add a class to the element
Sub AddClass(value As String)
    gb.AddClass(value)
End Sub

'set the selected index
Sub SetSelectedIndex(idx As Int)
    gb.addattribute("data-ej-selecteditemindex",idx)
End Sub

'add a button
Sub AddButton(bid As String, bCaption As String, imageURL As String, imageInternal As Boolean)
    If imageURL.Length > 0 Then
        If imageInternal = True Then
            imageURL = modHTML.WebViewAssetFile(imageURL)
        End If
    End If
    'build the image class for the button
    If imageURL.Length > 0 Then
        Dim imgclass As CssElement
        imgclass.Initialize("." & bid)
        imgclass.AddAttribute("background", "url(" & imageURL & ") no-repeat")
        imgclass.AddAttribute("background-position", "2px 50%")
        imgclass.AddAttribute("background-repeat", "no-repeat")
        imgclass.AddAttribute("background-size", "20px 20px")
        imgclass.AddAttribute("position", "relative")
        imgclass.AddAttribute("text-indent", "20px")
        page.AddStyle(imgclass.HTML)
    End If
    Dim lbl As HTMLElement
    Dim inp As HTMLElement
    'define the label
    lbl.Initialize("", "label")
    If imageURL.Length > 0 Then lbl.AddAttribute("data-ej-imageclass", bid)
    'define the type
    inp.Initialize("", "input")
    If MultipleSelect = True Then
        inp.AddAttribute("type","checkbox")
    Else
        inp.AddAttribute("type","radio")
    End If
    inp.AddAttribute("name", ID & "options")
    inp.AddContent(bCaption)
    lbl.AddContent(inp.HTML)
    'add the button to the group button
    gb.AddContent(lbl.HTML)
  
    'add css for button
    Dim cssb As CssElement
    cssb.Initialize("#" & bid)
    cssb.AddAttribute("padding-top","20px")
    page.addstyle(cssb.HTML)
End Sub

'return the html
Sub HTML As String
    If RenderMode.Length > 0 Then gb.AddAttribute("data-ej-rendermode", RenderMode)
    gbh.AddContent(gb.html)
    Return gbh.html
End Sub


'add the group button to the page
Public Sub AddToPage
    page.AddContent(HTML)
End Sub
Based on my example AMGroup component... my execution code looked like this..

B4X:
Sub Activity_Create(FirstTime As Boolean)
    'Do not forget to load the layout file created with the visual designer. For example:
    'Activity.LoadLayout("Layout1")
    Activity.Title = "AMGroupButton"
    'initialize a tile view
    pg.Initialize(Me,"pg")
    pg.AddToActivity(Activity,0dip,0dip,100%x,100%y)
    gb.Initialize(pg,"gb")
    gb.AddButton("all", "Apple", "ios.png", True)
    gb.AddButton("read", "Android","android.png",True)
    gb.AddButton("unread", "Windows", "windows.png", True)
    gb.AddToPage
    pg.Render
End Sub
And from there, the following HTML component was produced...

B4X:
<div id="gbsample" class="gbsample">
<div id="gb" data-role="ejmgroupbutton" data-ej-selecteditemindex="0" data-ej-touchend="groupbuttonclick" data-ej-rendermode="ios7">
<label data-ej-imageclass="all">
<input type="radio" name="gboptions">
Apple
</input>
</label>
<label data-ej-imageclass="read">
<input type="radio" name="gboptions">
Android
</input>
</label>
<label data-ej-imageclass="unread">
<input type="radio" name="gboptions">
Windows
</input>
</label>
</div>
<div id="gb" data-role="ejmgroupbutton" data-ej-selecteditemindex="0" data-ej-touchend="groupbuttonclick" data-ej-rendermode="ios7">
<label data-ej-imageclass="all">
<input type="radio" name="gboptions">
Apple
</input>
</label>
<label data-ej-imageclass="read">
<input type="radio" name="gboptions">
Android
</input>
</label>
<label data-ej-imageclass="unread">
<input type="radio" name="gboptions">
Windows
</input>
</label>
</div>
</div>
 

Mashiane

Expert
Licensed User
9. I finished creating my components, tested them if they are working including the events that I wanted to link to them, the setters and the getters and now it was the interesting part of distributing the library. There is a module that is here in the forum to extract files from jar files. We have tweaked this for our likes, so I discovered that I needed to do the same. To distribute my library this is what I did.

1. My library includes a tweaked version of the JarFileLoader, when the library is initialized, the js and css files are extracted from the library and put into temporal storage.
2. Due to permission issues, I also added RunTimePermissions

B4X:
ProgressMessage = "Working on it, please wait..."
    'extract the library contents
    jfl.Initialize(False)
    'jfl.AssetFromJar("bootstrap.min.css")
    jfl.AssetFromJar("ej.mobile.all.min.css")
    'jfl.assetfromjar("angular.min.js")
    'jfl.AssetFromJar("bootstrap.min.js")
    'jfl.AssetFromJar("ej.globalize.min.js")
    jfl.AssetFromJar("ej.mobile.all.min.js")
    jfl.AssetFromJar("ej.unobtrusive.min.js")
    'jfl.AssetFromJar("ej.widget.angular.min.js")
    'jfl.assetfromjar("ej.widget.ko.min.js")
    'jfl.AssetFromJar("excanvas.min.js")
    jfl.AssetFromJar("jquery.easing.1.3.min.js")
    jfl.AssetFromJar("jquery-3.1.1.min.js")
    jfl.AssetFromJar("jsrender.min.js")
    'jfl.AssetFromJar("knockout.min.js")
If you recall from above, when I init the page, I show the user a progress dialog, this can be turned on and off from the library by the way. Then each file is copied using assetfromjar, which is...

B4X:
Public Sub AssetFromJar(FileName As String) As Boolean
    FileName = FileName.tolowercase
    Try
        If File.Exists(targetFolder, FileName) = False Then
            Dim out As OutputStream = File.OpenOutput(targetFolder, FileName, False)
            File.Copy2(LoadFileFileFromJar(FileName), out)
            out.Close
            Return True
        Else
            Return True    ' File OK anyway
        End If
    Catch
        Log("Failed to copy file to Assets: " & FileName)
        Return False
    End Try
End Sub
I will include the tweaked version of this module here too, and finally...
 

Mashiane

Expert
Licensed User
10. Compile to Library, your jar and xml files will be saved in the additional libraries folder. For comments in the xml file, write some comments on your b4a code.

10.1 After your library is compiles, go to the libraries folder, rename the extension of your file from .jar to .zip
10.2 Double click the newly named .zip file and then add your jss and css files on the root of the library as depicted below.

library.png


Close the archiver and then rename your library file back to .jar file. Congratulations, you are done. Now create a project, reference your library and then develop the app you want based on the HTML, JavaScript and CSS library you just created.

That is how I created the AMTileView library.

Ta! Enjoy..

PS: Javascript/CSS libraries done in this fashion are a little slow (depending on the size of the js library you want to wrap) as they don't provide your native look and feel to your app. Yes there are some nice looking components you can use depending on what javascript/jquery framework you are looking at, but then I like the fact that you can trap events, change components of HTML elements from b4a.
 
Last edited:
  • Like
Reactions: eps
Top