I implemented LiveData & ViewModel to mimic AsyncTaskLoader.
I load file names from the camera directory in DCIM, and then i attach a fileObserver to Observe when a File (picture) is deleted, and then a callback tells the LiveData to re-fetch the fileNames when delete event occurs
The Problem:
The code below should fetch the file Names from DCIM/Pictures asynchronously with the help of LiveData and then a FileObserver is attached to the directory (DCIM/Pictures), to monitor when a file is deleted and a callback is implemented with the LiveData sub-class to reload the files, as demonstrated in code.
okay, it works the first time, that is, the files are loaded the first time, calling setValue() and passing the fileNames triggered onChange to be called in the observing Activity/Fragment. But when a file is deleted, the callback function calls the loadFiles() function to re-load the files again but calling the setValue and passing in the FileNames does not trigger OnChange in the observing Activity/Fragment this time around.
According to the official documentation of LiveData
You must call the setValue(T) method to update the LiveData object
from the main thread.
I am curious to know why LiveData is not updating its value after the first call.
The Code
MyLiveData
class MyLiveData() : MutableLiveData<MutableList<String>>(), PictureDelete {
override fun onPicDelete() {
loadFileNames()
}
val TAG = "MyLiveData"
val fileNamesList: MutableList<String> = ArrayList()
val fileWatcher : MyFileWatcher
init {
loadFileNames()
val pathToWatch = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM + "/Camera").getAbsolutePath()
fileWatcher = MyFileWatcher(pathToWatch, this)
fileWatcher.startWatching()
}
private fun loadFileNames() {
val fileDir: File
try {
fileDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM + "/Camera")
} catch (e: Exception) {
Log.e(TAG, e.message)
return
}
Log.d(TAG, "Actively Loading Files in Status LiveData")
val arrayOfFiles = fileDir.listFiles()
if (arrayOfFiles == null || arrayOfFiles.size < 1) return
Log.d(TAG, "Actively Loading Files. Size: ${arrayOfFiles.size}")
setValue(fileNamesList)
}
}
MyViewModel
class MyViewModel() : ViewModel() {
val myLiveData: MyLiveData
val TAG = "WhatsAppFragment-VModel"
init {
myLiveData = MyLiveData()
}
}
MyFragment
class MyFragment : Fragment() {
private val TAG = "MyFragment"
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_layout, container, false)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
viewModel.myLiveData.observe(this, androidx.lifecycle.Observer { fileNames ->
Log.d(TAG, "New Live Data Dispatch")
for ((index, name) in fileNames.withIndex()) {
Log.d(TAG, "the element at $index is $name")
}
})
}
}
MyFileObserver
class MyFileWatcher(pathToWatch: String, val picDelete: PictureDelete) : FileObserver(pathToWatch, DELETE) {
val TAG = "MyFileWatcher"
init {
Log.d(TAG, "Initialization")
}
override fun onEvent(event: Int, path: String?) {
if (event = FileObserver.DELETE) { // EventCode 512 == Delete
Log.d(TAG, "OnEvent. Event: $event Path: $path")
picDelete.onPicDelete()
}
}
}
PictureDelete Interface
interface PictureDelete {
fun onPicDelete()
}
What is wrong with my Implementation?
I have here an example #Micklo_Nerd but it does not use your problem of getting the files deleted but it gives you the idea for what you need to do.
In my example the user insert a name and after clicking a button, the list is changed.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:text="Add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/buttonAdd"
app:layout_constraintStart_toStartOf="#+id/filename"
app:layout_constraintEnd_toEndOf="#+id/filename"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="#+id/filename"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:ems="10"
android:id="#+id/filename"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/textView"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="#+id/buttonAdd"/>
</android.support.constraint.ConstraintLayout>
MyRepository (In your example is the MyLiveData)
In here you must do the work of getting the filenames in the folder, and put the in the MutableLiveData.
class MyRepository {
fun loadFileNames(liveData : MutableLiveData<MutableList<String>>, filename: String){
var fileList : MutableList<String>? = liveData.value
if(test == null)
fileList = MutableList(1){ filename }
else
fileList.add(filename)
liveData.value = fileList
}
}
MyViewModel
In here, I have two methods: one to update the list as I click the button and another to get the list of file names. You should probably only need the one that gets the list
class MyViewModel : ViewModel() {
val repo: MyRepository
var mutableLiveData : MutableLiveData<MutableList<String>>
init {
repo = MyRepository()
mutableLiveData = MutableLiveData()
}
fun changeList(filename: String){
repo.loadFileNames(mutableLiveData, filename)
}
fun getFileList() : MutableLiveData<MutableList<String>>{
return mutableLiveData
}
}
MainActivity
In here, you see I am observing the method that returns the list of filenames, which is what you need to do, because that is what is going to change.
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val TAG = "MyFragment"
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
viewModel.getFileList().observe(this, Observer<MutableList<String>> { fileNames ->
Log.d(TAG, "New Live Data Dispatch")
textView.text = ""
for ((index, name) in fileNames!!.withIndex()) {
textView.append("the element at $index is $name\n")
}
})
buttonAdd.setOnClickListener(this)
}
override fun onClick(v: View?) {
when(v!!.id){
R.id.buttonAdd -> {
viewModel.changeList(filename.text.toString())
filename.text.clear()
}
}
}
}
Hope this helps.
Related
I am developing an android app that has an activity with a recyclerview and an add button. When add button is clicked, a FragmentDialog is launched. the user enters the name of the book which will be stored in a greendao database. The list of books is displayed in the recyclerview.
I am using mvvm and livedata . the problem is that after adding the book in DialogFragment, the list is not updated, although the list is wrapped in livedata which is observed in activity.
DialogFragment:
// repository
val repository = Injection.provideRepository()
// viewmodel
val factory = CreateBookViewModelFactory(repository)
val viewModel =
ViewModelProvider(this, factory).get(BookViewModel::class.java)
builder.setView(binding.root)
// Add action buttons
.setPositiveButton(R.string.add,
DialogInterface.OnClickListener { dialog, id ->
// create book
book = Book(null, binding.name)
viewModel.onAddBook(book)
})
.setNegativeButton(R.string.cancel,
DialogInterface.OnClickListener { dialog, id ->
getDialog()?.cancel()
})
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
viewModel:
class BookViewModel(val repository: Repository) : ViewModel() {
// Create a LiveData
val _books = MutableLiveData<List<Book>>()
val books = repository.books
fun init() :MutableLiveData<List<Book>> {
_books.postValue(books)
return _books
}
}
activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_books)
binding.setLifecycleOwner(this)
binding.recyclerview.adapter = adapter
binding.recyclerview.setHasFixedSize(false)
// repository
val repository = Injection.provideRepository()
val factory = CreateBookViewModelFactory(repository)
val viewModel =
ViewModelProvider(this, factory).get(BookViewModel::class.java)
viewModel.init().observe(this, Observer {
it?.let {
adapter.submitList(Mapper.mapToBookListDTO(it))
}
})
binding.addReturn.setOnClickListener { view ->
val createFragment: DialogFragment = BookDialog()
createFragment.show(supportFragmentManager, "new Book")
}
}
When I add a new book and click the add button, the list is not updated. But when I reopen the activity the list of books get updated, it means the list is not observed for changes.
I am using the same viewModel for the activity and the DialogFragment. I also tried using seperate viewmodels for each, but same result.
Thanks a lot for your help!
I have implemented Tabbedpage using ViewModel but my ViewModel constructor call 4 times because I create 4 tabs, I also used prism for ViewModel binding.
Below is a design file
<?xml version="1.0" encoding="UTF-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
xmlns:material="clr-namespace:XF.Material.Forms.UI;assembly=XF.Material"
xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
xmlns:ffTransformations="clr-namespace:FFImageLoading.Transformations;assembly=FFImageLoading.Transformations"
prism:ViewModelLocator.AutowireViewModel="True"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns:extended="clr-namespace:Xamarin.Forms.Extended;assembly=Xamarin.Forms.Extended.InfiniteScrolling"
xmlns:customcontrols="clr-namespace:QuranicQuizzes.CustomControls"
xmlns:local="clr-namespace:QuranicQuizzes.Views" NavigationPage.HasNavigationBar="True"
x:Class="QuranicQuizzes.Views.DashboardPage">
<NavigationPage.TitleView>
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Label Text="Dashboard" TextColor="White" HorizontalTextAlignment="Center" HorizontalOptions="CenterAndExpand" VerticalTextAlignment="Center" FontFamily="{StaticResource QuranFontBold}" FontSize="Medium" />
<StackLayout Orientation="Horizontal">
<material:MaterialMenuButton x:Name="Menus" ButtonType="Text" Image="list" TintColor="White" BackgroundColor="Transparent" CornerRadius="24" Choices="{Binding Actions}" MenuSelected="MaterialMenuButton_MenuSelected" />
</StackLayout>
</StackLayout>
</NavigationPage.TitleView>
<local:HomeTabPage/>
<local:QuizzesTabPage/>
<local:LiveGameTabPage/>
<local:AssignmentTabPage/>
</TabbedPage>
Below is my code
public partial class DashboardPage : TabbedPage
{
private DashboardPageViewModel vm;
public DashboardPage()
{
try
{
InitializeComponent();
vm = BindingContext as DashboardPageViewModel;
}
catch (Exception ex)
{
}
}
}
Below is my ViewModel
public class DashboardPageViewModel : ViewModelBase
{
INavigationService _navigationService;
IClientAPI _clientAPI;
Dashboards dashboard;
public DashboardPageViewModel(INavigationService navigationService, IClientAPI clientAPI) : base(navigationService)
{
_navigationService = navigationService;
_clientAPI = clientAPI;
if (CrossConnectivity.Current.IsConnected)
{
var StartDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");
var Enddate = DateTime.Now.ToString("yyyy-MM-dd");
if (dashboard == null)
{
dashboard = new Dashboards();
getDashboardData(StartDate, Enddate);
}
}
}
}
I see what you're trying to do. You want to initialise your vm instance so that you can access you vm from your view.
Instead of doing this:
vm = BindingContext as DashboardPageViewModel;
what we can do is change the type of the existing BindingContext property by doing this:
public partial class DashboardPage
{
new DashboardPageViewModel BindingContext
{
get => (DashboardPageViewModel) base.BindingContext;
set => base.BindingContext = value;
}
public DashboardPage()
{
InitializeComponent();
}
}
now you can just access BindingContext.DoSomething because its type is now DashboardPageViewModel.
Now that's sorted out, your viewmodel should not be being called 4 times! Something is wrong here. Here is a checklist of things to do that may be causing the constructor being called 4 times as not a lot more info was provided.
Try removing <NavigationPage.TitleView> segment.
Make sure you are navigating to DashboardPage.
Make sure that each individual TabbedPage has it's own viewmodel.
Try removing prism:ViewModelLocator.AutowireViewModel="True"and manually adding the viewmodel to the TabbedPage.
Finally constructors should be able to run very fast and should only be used for assigning variables or instantiation or very quick operations. What you could maybe do is separate the code in your VM:
public class DashboardPageViewModel : ViewModelBase
{
IClientAPI _clientAPI;
Dashboards dashboard;
public DashboardPageViewModel(INavigationService navigationService, IClientAPI clientAPI) : base(navigationService)
{
_clientAPI = clientAPI;
}
public void Init()
{
if (CrossConnectivity.Current.IsConnected)
{
var StartDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");
var Enddate = DateTime.Now.ToString("yyyy-MM-dd");
if (dashboard == null)
{
dashboard = new Dashboards();
getDashboardData(StartDate, Enddate);
}
}
}
}
and then in your view you could add this method:
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if(BindingContext == null)
{
return;
}
BindingContext.Init();
}
I hope this really helps you.
NB: All this code was written on the fly and never compiled, there may be some errors.
The codes below runs in an android emulator with Android version SDK 16. But somehow when it runs on the emulator with Android version >21, DataBinding and Realm savings doesn't get triggered..
How should I be dealing with Android versioning?
Is it because the rendered UI differs in different android versions so DataBinding wouldn't have worked either?
This code here is the ViewModel
#PerFragment
public class OnboardDOBViewModel extends BaseHelperViewModel<BaseUserView> implements IOnboardDOBViewModel {
private String TAG = getClass().getSimpleName();
private HelperRepo mHelperRepo;
private UserRepo mUserRepo;
private Helper mHelper;
public final ObservableField<Integer> datePicker = new ObservableField<>();
public final ObservableField<Integer> monthPicker = new ObservableField<>();
public final ObservableField<Integer> yearPicker = new ObservableField<>();
public ReadOnlyField dobSelected;
private CompositeDisposable mCompositeDisposable = new CompositeDisposable();
#Inject
public OnboardDOBViewModel(#AppContext Context context, UserRepo userRepo, HelperRepo helperRepo) {
super(context, userRepo, helperRepo);
Log.d(TAG, "OnboardDOBViewModel: ");
this.mUserRepo = userRepo;
this.mHelperRepo = helperRepo;
}
#Override
public void attachView(BaseUserView view, #Nullable Bundle savedInstanceState) {
super.attachView(view, savedInstanceState);
Log.d(TAG, "attachView");
mHelper = mHelperRepo.getByUser("user.id", mUserRepo.getCurrentUser(), false);
if (mHelper != null) {
Calendar c = Calendar.getInstance();
yearPicker.set(c.get(Calendar.YEAR)-23);
monthPicker.set(c.get(Calendar.MONTH));
datePicker.set(c.get(Calendar.DAY_OF_MONTH));
}
Observable<String> dobSelected = Observable.combineLatest(
Utils.toObservable(monthPicker),
Utils.toObservable(datePicker),
Utils.toObservable(yearPicker),
(integer, integer2, integer3) -> {
String dobSelected1 = String.valueOf(monthPicker.get() + 1) + "-" +
datePicker.get().toString() + "-" +
yearPicker.get().toString();
Log.d(TAG, "apply: " + dobSelected1);
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy", Locale.getDefault());
Date date = sdf.parse(dobSelected1);
Log.d(TAG, "apply: date selected" + date);
Helper helper = mHelperRepo.getByUser("user.id", mUserRepo.getCurrentUser(), true);
assert helper != null;
helper.setDateOfBirth(date);
mHelperRepo.save(helper);
return dobSelected1;
});
this.dobSelected = Utils.toField(dobSelected);
}
This is the XML
<data>
<variable
name="vm"
type="...OnboardDOBViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="30dp"
android:text="#string/toolbar_title_date_of_birth"
android:textAppearance="#style/TextAppearance.AppCompat.Large" />
<DatePicker
android:id="#+id/onboard_birth_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:calendarViewShown="false"
android:day="#={vm.datePicker}"
android:month="#={vm.monthPicker}"
android:solidColor="#color/grayMedium"
android:spinnersShown="true"
android:year="#={vm.yearPicker}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="test"
android:text="#{vm.dobSelected}"
android:visibility="visible"/>
</LinearLayout>
Found this as my answer
OnDateChanged is not called in Date picker Android lollipop
added this in, and override the config, calendar UI is now spinner style and binding works
android:datePickerMode="spinner"
Requirement: When the user clicks on the TextView, a date picker should open up. The default date selected should be the date in the TextView. If the date is in the past, the DatePicker dialog's 'Set' button should be disabled. If the clickable TextView is empty, the default date in the DatePicker should be today's date.
This is a scenario I've already solved and am sharing here in order to help the Xamarin community. The code isn't very optimized, just FYI.
So, what we exactly need in this scenario is access to the event that the user is changing dates on the DatePicker Dialog. This can only be done if you use a DatePicker inside your own Dialog for more control. In my opinion, you cannot get access to this event if you use the default DatePickerDialog. Thus, we create a dialog extending the DialogFragment class and then implement the DatePicker inside of it. When the user clicks the TextView, we use show the fragment. Let's begin:
Here's the MainActivity:
using System;
using Android.App;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
using Java.Util;
using Java.Text;
namespace DatePickerTest
{
[Activity(Label = "DatePickerTest", MainLauncher = true, Icon = "#drawable/icon", Theme = "#android:style/Theme.Holo.Light")]
public class MainActivity : Activity
{
private string dueDate;
private TextView dateLabel;
private DateTime date;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
dateLabel = (TextView)FindViewById(Resource.Id.dateLabel);
dueDate = dateLabel.Text;
dateLabel.Click += delegate { ShowDialog(); };
}
public void ShowDialog()
{
var transaction = FragmentManager.BeginTransaction();
var dialogFragment = new mDialogFragment();
dialogFragment.Show(transaction, "dialog_fragment");
}
//Used for communication with the fragment
public string GetDueDate()
{
return dueDate;
}
//Used for communication with the fragment
public void SetDueDate(DateTime date)
{
//Additional check so that date isn't set in the past
if (date < DateTime.Now.Date)
Toast.MakeText(this, "Something went wrong! Please try again", ToastLength.Long).Show();
else
{
SimpleDateFormat MdyFormat = new SimpleDateFormat("MM/dd/yyyy");
dueDate = MdyFormat.Format(Date.Parse(date.ToString()));
dateLabel.Text = dueDate;
}
}
}
}
Main.axml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:id="#+id/MyButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="#string/Hello" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:background="#FAFAFA"
android:layout_height="wrap_content">
<TextView
android:id="#+id/dueDateLabel"
android:layout_height="45dp"
android:layout_width="wrap_content"
android:text="Due Date:"
android:padding="15dp"
android:textColor="#2E2E2E" />
<TextView
android:id="#+id/dateLabel"
android:layout_height="45dp"
android:layout_width="fill_parent"
android:hint="Some Date"
android:textColor="#2E2E2E"
android:text="03/16/2015" />
</LinearLayout>
</LinearLayout>
MDialogFragment.cs :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Android.Widget;
using Java.Util;
using Java.Text;
namespace DatePickerTest
{
public class mDialogFragment : DialogFragment
{
DatePicker picker;
private MainActivity MActivity;
private int Year, Month, Day;
private string DueDate;
private DateTime SelectedDueDate;
private string tempString = "";
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
//Get the instance of the MainActivity
MActivity = (MainActivity) this.Activity;
//Get the currently set due date
DueDate = MActivity.GetDueDate();
//Get instance of the Calendar
Calendar Today = Calendar.Instance;
//Update the class variables
Year = Today.Get(Calendar.Year);
Month = Today.Get(Calendar.Month);
Day = Today.Get(Calendar.Date);
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
//Inflating the dialog layout
var view = inflater.Inflate(Resource.Layout.MDialogLayout, container, false);
//Finding all the views in it:
var cancel = (Button)view.FindViewById(Resource.Id.cancel);
var set = (Button)view.FindViewById(Resource.Id.set);
picker = (DatePicker)view.FindViewById(Resource.Id.pickerdate);
//DatePicker flag to make it look like the default DatePicker
picker.CalendarViewShown = false;
//Checking to see if current date is in the past, if YES, disable the 'Set' button
if ((DateTime.Parse(DueDate) < DateTime.Now)) { set.Enabled = false; }
//Initate the picker with the current due date OR today's date
picker.Init(GetDefaultYear(), GetDefaultMonth(), GetDefaultDayOfMonth(), new onDateChangedListener((picker1, year, month, day) =>
{
//Getting the DatePicker value in a string
tempString = (month + 1) + "/" + day + "/" + year;
//Parsing the value into a variable
SelectedDueDate = (DateTime.Parse(tempString).Date);
//Setting the MDatePicker dialog's Title
Dialog.SetTitle(GetDateDetails(SelectedDueDate));
//Enable/Disalbe 'Set' button depending on the condition
if (SelectedDueDate >= DateTime.Now.Date)
set.Enabled = true;
else
set.Enabled = false;
}));
//Setting Dialog Title for the first time when it opens
Dialog.SetTitle(GetDateDetails(DateTime.Parse(DueDate)));
//Click function for Cancel button
cancel.Click += delegate{Dismiss();};
//Click function for Set button
set.Click += (object sender, EventArgs e) =>
{
SetSelectedDueDate(sender, e);
};
return view;
}
private string GetDateDetails(DateTime date)
{
string DateDetails;
Calendar cal = Calendar.Instance;
SimpleDateFormat DayOfWeekFormat = new SimpleDateFormat("EEE");
SimpleDateFormat MonthFormat = new SimpleDateFormat("MMM");
DateDetails = DayOfWeekFormat.Format(Date.Parse(date.ToString())) + ", " + date.Day + " " + MonthFormat.Format(Date.Parse(date.ToString())) + " " + date.Year;
return DateDetails;
}
private void SetSelectedDueDate(object sender, EventArgs e)
{
MActivity.SetDueDate(SelectedDueDate);
Dismiss();
}
private int GetDefaultMonth()
{
//The currently set due date is in the format "MM/DD/YYYY"
if(MActivity.GetDueDate()==null || MActivity.GetDueDate() == "")
return Month;
return Convert.ToInt32(MActivity.GetDueDate().Substring(0, 2)) - 1;
}
private int GetDefaultDayOfMonth()
{
if (MActivity.GetDueDate() == null || MActivity.GetDueDate() == "")
return Day;
return Convert.ToInt32(MActivity.GetDueDate().Substring(3, 2));
}
private int GetDefaultYear()
{
if (MActivity.GetDueDate() == null || MActivity.GetDueDate() == "")
return Year;
return Convert.ToInt32(MActivity.GetDueDate().Substring(6, 4));
}
}
//We need this class and interface implementation to create and Init the DatePicker
class onDateChangedListener : Java.Lang.Object, DatePicker.IOnDateChangedListener
{
Action<DatePicker, int, int, int> callback;
public onDateChangedListener(Action<DatePicker, int, int, int> callback)
{
this.callback = callback;
}
public void OnDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth)
{
callback(view, year, monthOfYear, dayOfMonth);
}
}
}
MDialogLayout.axml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<DatePicker
android:id="#+id/pickerdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:id="#+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancel"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle" />
<Button
android:id="#+id/set"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SET"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:paddingTop="1dp" />
</LinearLayout>
</LinearLayout>
I am developing an android application which is purely web based. It needs to display HTML5 based webpages using webview which is rich in multimedia contents. when I try to play video content embedded in the webpage its all gone fine and i am also able to play video in full screen.
But my main problem starts here: When I toggled the device screen while playing fullscreen video, it suddenly exits from playback to its current web page. I am stucked here and here I am including all codes that I used as I can in hopes someone can help me.
Activity that is used: MainActivity.java
`
public class MainActivity extends Activity {
private WebView webView;
private FrameLayout customViewContainer;
private WebChromeClient.CustomViewCallback customViewCallback;
private View mCustomView;
private MyWebViewClient mWebViewClient;
private MyWebChromeClient mWebChromeClient;
/**
* Called when the activity is first created.
*/
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customViewContainer = (FrameLayout) findViewById(R.id.customViewContainer);
webView = (WebView) findViewById(R.id.webView);
mWebViewClient = new MyWebViewClient();
webView.setWebViewClient(mWebViewClient);
mWebChromeClient = new MyWebChromeClient();
webView.setWebChromeClient(mWebChromeClient);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setAppCacheEnabled(true);
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setSaveFormData(true);
webView.loadUrl("http://yuotube.com");
}
public boolean inCustomView() {
return (mCustomView != null);
}
public void hideCustomView() {
mWebChromeClient.onHideCustomView();
}
#Override
protected void onPause() {
super.onPause(); //To change body of overridden methods use File | Settings | File Templates.
webView.onPause();
}
#Override
protected void onResume() {
super.onResume(); //To change body of overridden methods use File | Settings | File Templates.
webView.onResume();
}
#Override
protected void onStop() {
super.onStop(); //To change body of overridden methods use File | Settings | File Templates.
if (inCustomView()) {
hideCustomView();
}
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (inCustomView()) {
hideCustomView();
return true;
}
if ((mCustomView == null) && webView.canGoBack()) {
webView.goBack();
return true;
}
}
return super.onKeyDown(keyCode, event);
}
// WebChromeClient Class
public class MyWebChromeClient extends WebChromeClient {
private Bitmap mDefaultVideoPoster;
private View mVideoProgressView;
#Override
public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) {
onShowCustomView(view, callback); //To change body of overridden methods use File | Settings | File Templates.
}
#Override
public void onShowCustomView(View view,CustomViewCallback callback) {
// if a view already exists then immediately terminate the new one
if (mCustomView != null) {
callback.onCustomViewHidden();
return;
}
mCustomView = view;
webView.setVisibility(View.GONE);
customViewContainer.setVisibility(View.VISIBLE);
customViewContainer.addView(view);
customViewCallback = callback;
}
#Override
public View getVideoLoadingProgressView() {
if (mVideoProgressView == null) {
LayoutInflater inflater = LayoutInflater.from(MainActivity.this);
mVideoProgressView = inflater.inflate(R.layout.video_progress, null);
}
return mVideoProgressView;
}
#Override
public void onHideCustomView() {
super.onHideCustomView(); //To change body of overridden methods use File | Settings | File Templates.
if (mCustomView == null)
return;
webView.setVisibility(View.VISIBLE);
customViewContainer.setVisibility(View.GONE);
// Hide the custom view.
mCustomView.setVisibility(View.GONE);
// Remove the custom view from its container.
customViewContainer.removeView(mCustomView);
customViewCallback.onCustomViewHidden();
mCustomView = null;
}
}
// WebViewClient Class
public class MyWebViewClient extends WebViewClient {
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url); //To change body of overridden methods use File | Settings | File Templates.
}
ProgressDialog dialog = ProgressDialog.show(MainActivity.this, "",
"Loading multimedia! Please wait...", true);
#Override
public void onPageFinished(WebView view, String url) {
dialog.dismiss();
}
}
}
web_activity.xml
<!-- View that will be hidden when video goes fullscreen -->
<RelativeLayout
android:id="#+id/nonVideoLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.iptvmodified.VideoEnabledWebView
android:id="#+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
<!-- View where the video will be shown when video goes fullscreen -->
<RelativeLayout
android:id="#+id/videoLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- View that will be shown while the fullscreen video loads (maybe include a spinner and a "Loading..." message) -->
<View
android:id="#+id/videoLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="invisible" />
</RelativeLayout>
`activity_main.xml
<RelativeLayout 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"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/hello_world" />
I am searching for a solution for thisand googled this to death with no progress made. Any help would be greatly appreciated.
Normally when the device is rotated your activity gets destroyed and recreated. This is probably causing the WebView to get 'kicked out of' fullscreen playback.
You need to override Activity.onConfigurationChanged and declare you want to handle orientation changes in your manifest. See the docs for details.