Hello Friends, Last time we saw how to create make a PDF view and a thumb view for iOS and use it to open a PDF in Xamarin Forms. Now we are going to see how to do the same thing with Android. In brief, we will create a custom renderer based on Radaee PDF’s PDF view and thumb view. The creation process of these views includes some tricks I’ll share in this article.
Getting Started with Radaee PDF for Android
RadaeePDF is a library that provides PDF manipulation functionalities to mobile apps. It exists both for Android and iOS. It isn’t a free library, and its documentation isn’t very detailed. But on the other side, It is available as a one-time license (And not per user license as what several PDF solutions offer), and it has Xamarin Bindings available on Github.
This library isn’t available as a NuGet package. The team in charge of this lib only made a project with Xamarin Android and Xamarin iOS bindings on Github. So, you might need to know a little bit about creating and building Xamarin Native bindings Libraries (Check this doc out to learn about binding native libraries) if you find difficulties generating the dll on your own.
- Reference the Radaee library into your Xamarin.Android project. You can get the library as I mentioned above.
- Contact the RadaeePDF team and get a license if you need it,
- Once you get the api keys, open your main activity, and add the following code in the “OnCreate” method after initializing Xamarin Forms
1 2 3 | var mPdfManager = new RadaeePDFManager(); mPdfManager.ActivateLicense(this, 0, "CompanyName", "Email", "Key"); |
Building Android’s PDF View with Radaee PDF
We will now proceed and create the custom PDF control needed to open a PDF in Xamarin Forms Pages. For this, we will use Custom renderers. To learn more about custom renderers, you can follow this link.
- First, in our shared project, we create a PDFReaderView, that will serve as the basis of our custom renderer. The code for this class is found in the previous article.
- Now, in the Android project, we will build our custom renderer based on Radaee’s PDFLayoutView. But before we do that, we need to wrap this PDFLayoutView inside a custom android view built with xml, because the PDFLayoutView needs inflation.
- Create an xml layout, with a container that contains the PDFLayout view as its only view.
- Then create a class that inflates this layout, and gets the PDFLayout, and exposes it as a property. You can see all of these below:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <com.radaee.reader.PDFLayoutView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:id="@+id/pdf_reader_view"/> </FrameLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class PDFReader : FrameLayout { public Com.Radaee.Reader.PDFLayoutView RadaeePDFReaderView { get; private set; } protected PDFReader(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { InitView(); } public PDFReader(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) { InitView(); } public PDFReader(Context context, IAttributeSet attrs) : base(context, attrs) { InitView(); } public PDFReader(Context context) : base(context) { InitView(); } private void InitView() { var view = Inflate(Context, Resource.Layout.radaee_pdf_viewer_layout, this); RadaeePDFReaderView = this.FindViewById<Com.Radaee.Reader.PDFLayoutView>(Resource.Id.pdf_reader_view); } } |
- We then create our custom renderer with the wraper control we just created above.
- In our custom renderer, we configure the exposed pdflayoutview above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | [assembly: ExportRenderer(typeof(PDFReaderView), typeof(RadaeePDFViewerRenderer))] namespace MobileApp.Droid.CustomRenderers { public class RadaeePDFViewerRenderer : ViewRenderer<PDFReaderView, CustomControls.PDFReader>, ILayoutViewPDFLayoutListener { Document _document; PDFReaderView PdfReaderView => Element as PDFReaderView; ILogger<RadaeePDFViewerRenderer> _logger; public RadaeePDFViewerRenderer(Context context) : base(context) { _document = new Document(); _logger = MobileApp.Setup.ServiceProvider.GetRequiredService<ILogger<RadaeePDFViewerRenderer>>(); } protected override void OnElementChanged(ElementChangedEventArgs<PDFReaderView> e) { Global.Init((Android.App.Activity)Context); Global.DebugMode = false; CreateNativeControl(e.NewElement); } void CreateNativeControl(PDFReaderView e) { if (Control == null) { var radaeePdfViewer = e; var radaeePdfControl = new PDFReader(Context); if (!string.IsNullOrEmpty(radaeePdfViewer.FilePath) && !string.IsNullOrEmpty(radaeePdfViewer.Password)) { try { Open(radaeePdfControl.RadaeePDFReaderView, Element.FilePath, Element.Password); } catch (Exception ex) { _logger.LogCritical(ex, "Fatal PDF error"); } } if (Element.CurrentPage > 0) { radaeePdfControl.RadaeePDFReaderView.PDFGotoPage(Element.CurrentPage); } SetNativeControl(radaeePdfControl); } } void Open(PDFLayoutView control, string path, string password) { if (!string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(password)) { _document ??= new Document(); _document.Close(); var ret = _document.Open(path, password); //switch (ret) //{ // case -1://need input password // break; // case -2://unknown encryption // break; // case -3://damaged or invalid format // break; // case -10://access denied or invalid file path // break; // case 0://succeeded, and continue // break; // default://unknown error // break; //} if (ret != 0) _logger.LogError($"Failed to open PDF error code: {ret} (#Check Source code for error code meaning) Product File Path: {path}"); if (Element.PageCountSetCommand != null && Element.PageCountSetCommand.CanExecute(_document.PageCount)) Element.PageCountSetCommand.Execute(_document.PageCount); control.PDFOpen(_document, this); //Set the PDF's display type, control.PDFSetView(6); /* * * Source: https://www.radaeepdf.com/forum/Android-development/2285-what-is-the-use-of-m-reader-pdfsetview-x 0 -> Vertical 1 -> Horizontal 2 -> Curl 3 -> Single 4 -> show 2 pages as 1 page in landscape 5 -> Reflow 6 -> One page at a time, horizontally */ } } void ZoomPage(int pageNumber) { if (_document != null) { var y = _document.GetPageHeight(pageNumber); var x = _document.GetPageWidth(pageNumber); Zoom(x / 2, y / 2, 1); } } void Zoom(float x, float y, int zoomLevel) { Global.ZoomLevel = zoomLevel; Control.RadaeePDFReaderView.PDFSetZoom((int)x, (int)y, Control.RadaeePDFReaderView.PDFGetPos((int)x, (int)y), Global.ZoomLevel + Global.ZoomStep); } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); try { CreateNativeControl(Element); if (Control != null && Element != null) { if (e.PropertyName == nameof(PdfReaderView.CurrentPage)) { Control.RadaeePDFReaderView.PDFGotoPage(Element.CurrentPage); } else if (e.PropertyName == nameof(PdfReaderView.BookMarkPage)) { Control.RadaeePDFReaderView.PDFGotoPage(Element.BookMarkPage); } else if (e.PropertyName == nameof(Element.FilePath) || e.PropertyName == nameof(Element.Password)) { Open(Control.RadaeePDFReaderView, Element.FilePath, Element.Password); } } } catch (ObjectDisposedException ex) { _logger.LogError(ex, $"Object was disposed while setting property: {e.PropertyName}"); } catch (Exception ex) { _logger.LogCritical(ex, "Fatal PDF error"); } } protected override void Dispose(bool disposing) { try { if (_document != null) { _document.Close(); _document = null; } Global.RemoveTmp(); Control.Dispose(); base.Dispose(disposing); } catch (Exception ex) { return; } } public void OnPDFAnnotTapped(int p0, Com.Radaee.Pdf.Page.Annotation p1) { ; } public void OnPDFBlankTapped() { if (PdfReaderView.PDFTappedCommand != null && PdfReaderView.PDFTappedCommand.CanExecute(null)) { PdfReaderView.PDFTappedCommand.Execute(null); } } public void OnPDFPageChanged(int page) { if (PdfReaderView.PageChangedCommand != null && PdfReaderView.PageChangedCommand.CanExecute(page)) { PdfReaderView.PageChangedCommand.Execute(page); } } public bool OnPDFDoubleTapped(float p0, float p1) { return true; } public void OnPDFLongPressed(float p0, float p1) { ; } public void OnPDFOpen3D(string p0) { ; } public void OnPDFOpenAttachment(string p0) { ; } public void OnPDFOpenJS(string p0) { ; } public void OnPDFOpenMovie(string p0) { ; } public void OnPDFOpenSound(int[] p0, string p1) { ; } public void OnPDFOpenURI(string p0) { ; } public void OnPDFPageDisplayed(Canvas p0, ILayoutViewVPage p1) { ; } public void OnPDFPageModified(int p0) { ; } public void OnPDFPageRendered(ILayoutViewVPage p0) { ; } public void OnPDFSearchFinished(bool p0) { ; } public void OnPDFSelectEnd(string p0) { ; } public void OnPDFZoomEnd() { ; } public void OnPDFZoomStart() { ; } } } |
Building the Thumb view
The process of building the thumb view is very similar to the one described above.
- We create a wrapper view, to inflate the thumbview
- Firist, we make the xml layout of the wrapper view
- Second, we create a class to inflate this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <com.radaee.util.PDFThumbView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:id="@+id/thumb_view"/> </FrameLayout> |
If you find this article useful, please follow me on Twitter,  Github, Linkedin, or like my Facebook page to stay updated.Follow me on social media and stay updated
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class PDFThumbView : FrameLayout { public Com.Radaee.Util.PDFThumbView RadaeePDFThumbView { get; private set; } protected PDFThumbView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { InitView(); } public PDFThumbView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) { InitView(); } public PDFThumbView(Context context, IAttributeSet attrs) : base(context, attrs) { InitView(); } public PDFThumbView(Context context) : base(context) { InitView(); } private void InitView() { var view = Inflate(Context, Resource.Layout.thumb_view, this); RadaeePDFThumbView = this.FindViewById<Com.Radaee.Util.PDFThumbView>(Resource.Id.thumb_view); } } |
- We use this wrapper view to create our custom renderer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | [assembly: ExportRenderer(typeof(PDFReaderThumbView), typeof(RadaeePDFThumbViewRenderer))] namespace MobileApp.Droid.CustomRenderers { public class RadaeePDFThumbViewRenderer : ViewRenderer<PDFReaderThumbView, CustomControls.PDFThumbView>, IPDFThumbListener { Document _document; PDFReaderThumbView ReaderThumbView => Element as PDFReaderThumbView; ILogger<RadaeePDFThumbViewRenderer> _logger; public RadaeePDFThumbViewRenderer(Context context) : base(context) { _document = new Document(); _logger = MobileApp.Setup.ServiceProvider.GetRequiredService<ILogger<RadaeePDFThumbViewRenderer>>(); } protected override void OnElementChanged(ElementChangedEventArgs<PDFReaderThumbView> e) { Global.Init((Android.App.Activity)Context); Global.DebugMode = false; if (Control == null) { var radaeeThumbView = e.NewElement; var ourThumbview = new CustomControls.PDFThumbView(Context); var radaeePdfThumbView = ourThumbview.RadaeePDFThumbView; if (!string.IsNullOrEmpty(radaeeThumbView.FilePath)) { Open(radaeePdfThumbView, Element.FilePath, Element.Password); } if (radaeeThumbView.CurrentPage > 0) { radaeePdfThumbView.ThumbGotoPage(radaeeThumbView.CurrentPage); } SetNativeControl(ourThumbview); } } void Open(PDFThumbView control, string path, string password) { if (!string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(password)) { _document.Close(); var ret = _document.Open(path, password); //switch (ret) //{ // case -1://need input password // break; // case -2://unknown encryption // break; // case -3://damaged or invalid format // break; // case -10://access denied or invalid file path // break; // case 0://succeeded, and continue // break; // default://unknown error // break; //} if (ret != 0) _logger.LogError($"Failed to open PDF error code: {ret} (#Check Source code for error code meaning) Product File Path: {path}"); control.ThumbOpen(_document, this, false); } } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); try { if (Control != null) { if (e.PropertyName == nameof(ReaderThumbView.CurrentPage)) { Control.RadaeePDFThumbView.ThumbGotoPage(Element.CurrentPage); } else if (e.PropertyName == nameof(Element.FilePath) || e.PropertyName == nameof(Element.Password)) { Open(Control.RadaeePDFThumbView, Element.FilePath, Element.Password); } } } catch (ObjectDisposedException ex) { _logger.LogError(ex, $"Object was disposed while setting property: {e.PropertyName}"); } } protected override void Dispose(bool disposing) { try { if (_document != null) { _document.Close(); _document = null; } Global.RemoveTmp(); Control.Dispose(); base.Dispose(disposing); } catch (Exception ex) { return; } } public void OnPageClicked(int page) { if (ReaderThumbView.PageChangedCommand != null && ReaderThumbView.PageChangedCommand.CanExecute(page)) { ReaderThumbView.PageChangedCommand.Execute(page); } } } } |
Using these controls in the shared project is easy. Inside your XAML page, you will just need to reference the namespaces, and call the controls as follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <customviews:PDFReaderView BookMarkPage="{Binding BookmarkPage}" PageChangedCommand="{Binding PageChangedCommand}" PageCountSetCommand="{Binding PageCountSetCommand}" PDFTappedCommand="{Binding PDFTappedCommand}" FilePath="{Binding FilePath}" Password="{Binding PassWord}" CurrentPage="{Binding CurrentPage}" Grid.Row="0" Grid.RowSpan="3" ThumbNailsCreatedCommand="{Binding ThumbnailCreatedCommand}" Margin="0,0,0,0" x:Name="PDFReaderView"/> <customviews:PDFReaderThumbView IsVisible="{OnPlatform Default=False, Android=True}" FilePath="{Binding FilePath}" Password="{Binding PassWord}" PageChangedCommand="{Binding PageChangedCommand}" CurrentPage="{Binding CurrentPage}" x:Name="AndroidThumbView" Grid.Row="2" VerticalOptions="Start" Margin="0,0,0,5"/> |
Conclusion
We saw how to build a PDF reader control, and implant it into our Xamarin Forms pages. This can be handy for specific scenarios. Today we used Radaee PDF, but later I might share a post about another library, that is more stable, and free. If you want to build a custom PDF control and thumb view on iOS, you can do so with PDFKit this blog post will show you how.
Follow me on social media and stay updated