How to persist the state of a custom Android View? - android

I want to manipulate a library I just discovered to drag/rotate and pinch zoom images. I am using two sources:
-The library itself: MultiTouchController.java https://code.google.com/p/android-multitouch-controller/source/browse/MTController/src/org/metalev/multitouch/controller/MultiTouchController.java
-An implementation of this library: PhotoSortrView https://code.google.com/p/android-multitouch-controller/source/browse/MTPhotoSortr/src/org/metalev/multitouch/photosortr/PhotoSortrView.java
-an example of Activity which implements this source:
public class PhotoSortrActivity extends Activity {
PhotoSortrView photoSorter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setTitle(R.string.instructions);
photoSorter = new PhotoSortrView(this);
setContentView(photoSorter);
}
#Override
protected void onResume() {
super.onResume();
photoSorter.loadImages(this);
}
#Override
protected void onPause() {
super.onPause();
photoSorter.unloadImages();
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
photoSorter.trackballClicked();
return true;
}
return super.onKeyDown(keyCode, event);
}
}
The problem is the activity doesn't remember the position of the images when it leaves the foreground and I recall it after. Worse, the images are set randomly each time the Activity is called.
How can I make the Activity remember the state of the images? (the specific issue in my case is that it is a custom view)
Here is an example of what I want the Activity to remember, and then the reset made by the Activity after being called again:

See the savedInstanceState parameter in onCreate()? You override onSaveInstanceState() to set up a bundle that remembers your Activity state, and it shows up in the onCreate().
Now, if your question is: How do you save Activity state without changing the PhotoSortrView class; well, you can't. You would have to add some state parameter to loadImages() so that it loads the images and sets the current image, and maybe change unloadImages() so that it returns some state information that the Activity can save.

Related

Issue in using onStop() and onResume()

I am using onStop() to save a boolean value which I need when the activity resumes.
Here is my code:
#Override
protected void onStop()
{
super.onStop();
Bundle bundle = new Bundle();
bundle.putBoolean("value",value);
getIntent().putExtras(bundle);
}
#Override
protected void onResume()
{
super.onResume();
if(getIntent().getExtras() != null)
{
Bundle bundle = getIntent().getExtras();
value = bundle.getBoolean("value");
}
}
My issue is no matter what the value of the boolean is, my onResume() always retrieves it as FALSE. This issue only occurs if I leave my activity using the BACK button. If I press home, things seem to work fine(i.e if the boolean was TRUE then onResume() retrieves it as TRUE.
Please do help me because I don't understand why onResume() always gets the value of the boolean as FALSE even when I save it as TRUE in onStop().
I also tried onRestart(), onPause() and onBackPressed() but I still can't get the proper boolean value to be saved.
You have two issues here.
the correct way to save values during activity destruction is to use onSaveInstanceState(Bundle) and get the value from the Bundle passed to onCreate(Bundle).
Check example below:
public class SavedInstanceExample extends AppCompatActivity {
private boolean myBoolean;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_savded_instace_example);
if (savedInstanceState != null) {
myBoolean = savedInstanceState.getBoolean("key");
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("key", myBoolean);
}
}
When you press the back button the activity will be finished. That means completely gone. And values saved one the methods explained above will not be there.
The way to save something to survive the Activity being finished is to save it to the disk. One common/simple way to do it is using the SharedPreferences
When the Android application opens the following activity lifecycle methods will be called.
onCreate();
onStart();
onResume();
and when you press the back button, the application will be destroyed by calling following methods
onPause();
onStop();
onDestroy();
And in the second case when you press home button the following methods will be called
onPause();
onStop();
That means your application is not destroyed completely and you can open it from recent apps so that the activity re-appears by calling
onStart();
onStop();
That is why your code works in this case.
Activity gives onSavedInstanceState() method to save your data during configuration changes or something else.
Here is the link for Android documentation for
Activity
I would suggest you to read the Google Developers Link for Activity documentation.Google Developers Activity
The OP's code is basically right. onSavedInstanceState is no good if you are not destroying the activity but, for example, replacing a fragment in an activity with another fragment and then returning to it, in which case you have to use onStop and onResume as follows. This is Kotlin and it works but the principle is the same.
override fun onStop() {
super.onStop()
val bundle = Bundle()
bundle.putBoolean("BOOL", false)
activity?.intent?.putExtras(bundle)
}
override fun onResume() {
super.onResume()
if (activity?.intent?.extras != null) {
val bundle = activity?.intent?.extras
val bool = bundle?.getBoolean("BOOL")
println("BOOL is $bool")
}
}
My Java's a bit rusty but I suspect the OP's problem might have been that he mixed up boolean and Boolean?

Generic handle OnBackPressed()

I have a BaseActivity() that have many activities and a BaseFragment() that have many fragments. Each activity contains 2-3 fragments and I need to make a generic method to handle each onBackPressed from all fragments (all - means all app screens) but this method should be in Base Fragment() (every fragment extends it). I supose that I'll need a kind of listener to tie OnBackPressed() from BaseActivity() to genericMethod() from BaseFragment()
Thanks in advice.
#Choletski:
onBackPressed()
It will be called when the activity has detected the user's press of the back key. The default implementation simply finishes the current activity, but you can override this to do whatever you want.while overriding the default back button action as it is not suggested to change the android default user experience.
Override the onBackPressed() method and take the action inside this function.
#Override
public void onBackPressed() {
// Write your code here
super.onBackPressed();
}
How to implement onBackPressed() in Android Fragments?
The simplest solution rest to be a bit "hard programmed" in my case, like I mentioned in my question I need a method from BaseFragment() to handle all back press actions from all screens that means all fragments that extends this BaseFragment().
#Sharp Edge solution may be accepted but why to handle it in each SimpleActivity() that extends BaseActivity() if I can just add a single method in BaseFragment() and all simple activities that extends BaseActivity() will don't care about that.
#escape-llc solution is confused and not the expected one... I can handle it easier using EventBus or Otto and send from onResume() from each fragment to SimpleActivity(). So I'll receive the actual open fragment and I'll now what action to do when onBackPressed() is executed...
So, like I said, my solution is to use just a simple generic method in BaseFragment():
public void doBackBtnPressedAction(View view) {
view.setFocusableInTouchMode(true);
view.requestFocus();
view.setOnKeyListener(new View.OnKeyListener() {
#Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
//logical part, in my case some server requests
return true;
}
}
return false;
}
});
}
This is how i handled it when i had a webview in fragment and wanted to handle onBackPressed for the webview?
public class Tab2 extends Fragment {
ProgressBar progress;
WebView x;
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View v =inflater.inflate(R.layout.tab_2,container,false);
x = (WebView)v.findViewById(R.id.webView);
x.setOnKeyListener(new OnKeyListener() {
#Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if(event.getAction() == KeyEvent.ACTION_DOWN)
{
WebView web = (WebView)v;
switch (keyCode)
{
case KeyEvent.KEYCODE_BACK:
if(web.canGoBack())
{
web.goBack();
return true;
}
break;
}
}
return false;
}
});
You have to make a custom Activity class for this.. and override its on onBackPressed() and add your logic in their. Then make sure wherever Fragments are used, you have to make the associated Activity sub class of this CustomActivity..
So whenever no matter on which Fragment user is, onBackPressed() of that Activity will be called and add super() to it.. so that it will call the base class's method and your code will run on each fragment.
example:
MyCustomActvity extends FragmentActivity{
#Override
public void onBackPressed(){
// your logic here
super.onBackPressed();
}
}
Now You know that Fragments must have at least 1 Base Activity, so just override that Activity's onBackPressed()
MyActivity extends MyCustomActivity{
// 3 fragments are called/replaced from this activity
// other code
#Override
public void onBackPressed(){
super.onBackPressed(); // it will invoke base class method and your code
}
}
Just extend MyCustomActivity for the ones which use Fragments.
Here is a great way to handle it in a general fashion. We use it now in all of our fragment-based apps.
First create an interface for fragments to implement. This represents whether they want to handle the back key at all. If not, don't implement the interface.
public interface IHandleBackPressed {
boolean handleBackPressed(Activity ax);
}
This is essentially a proxy for the activity's onBackPressed method.
Next, override the Activity.onBackPressed method with this boilerplate:
#Override
public void onBackPressed() {
final Fragment fx = getFragmentManager().findFragmentById(R.id.content);
if(fx != null) {
if(fx instanceof IHandleBackPressed) {
final IHandleBackPressed ihbp = (IHandleBackPressed)fx;
if(ihbp.handleBackPressed(this)) {
// we handled it
return;
}
}
}
// onBackPressed unhandled by us
super.onBackPressed();
}
This can be the same always. If you have multiple fragment areas, simply repeat the sequence for each one. If you have additional logic, integrate it before or after, but before you call super.onBackPressed to let the system take over (i.e. exit your activity).
Here is a sample of what a Fragment can do. This example uses a WebView and it wants to "use" the back key to manage the Back Stack of the WebView:
public class BrowseUrlFragment extends Fragment implements IHandleBackPressed {
WebView wv;
public boolean handleBackPressed(Activity ax) {
if(wv != null && wv.canGoBack()) {
wv.postDelayed(goback, 150);
return true;
}
return false;
}
}

Is there a better way of checking how activity was finished in onDestroy than setting a flag?

I want to call foo() every time my activity is destroyed, unless it is destroyed as result of clicking on a specific menu option (that eventually calls finish()). Currently I do this by calling foo() on default in onDestroy, unless a FLAG is set to true, where FLAG is set when I intercept the click on the menu option.
Is there a better way of doing this than setting a flag? Perhaps some way I can attach a tag to Android's finish() so that I can see the reason? Normally I would just try to call foo where it applies, but I can't account for every reason an activity might be destroyed.
Further caveat is that I would prefer not to make changes to base class (RootActivity)
public abstract class RootActivity extends Activity{
private flag someCondition;
#Override
protected void onCreate(Bundle savedInstanceState){
//...
}
// ...
public void startJob(JobAction.Id jobaction){
if (!jobaction.someCondition){
return;
}else{
startSomeLongAsynchronousJob(someCondition);
finish(); //If this is why onDestroy happened in subclass, I dont want to call foo()
}
}
#Override
public boolean onOptionsItemSelected(MenuItem item){
//...
startJob(JobAction.SOMEENUM); //Startjob is being called in the superclass
}
//...
}
public class SpecificJob extends SomeClassThatExtendsRoot{
private boolean FLAG = false;
#Override
public void onCreate(Bundle bundle){
super.onCreate(bundle);
//...
}
// ... some code ...
#Override
public onDestroy(){
if (!FLAG){ //Check if it was finish() that did this
foo();
}
super.onDestroy();
}
#Override
onOptionsItemSelected(MenuItem item){
super.onOptionsItemSelected(item);
if (item.getItemId()==r.id.DONTCALLFOO){
flag=true;
}
}
}
You have to override onSaveInstanceState in your activity.
#Override
protected void onSaveInstanceState(Bundle outState) {
Log.d("ApplicationFlow","onSaveInstanceState was called. System destroy your activity");
foo();
super.onSaveInstanceState(outState);
}
It is always called when the activity is destroyed by the system, and not by you (when you call finish()).
The purpose is give to user a chance to save some state in Bundle outState parameter. This bundle will be passed to onCreate(Bundle savedInstanceState) to the user restore the state, when the activity is going to be recreated.
See documentation

The AsyncTask fails when I rotate the device to landscape

I have an Activity in which I have a ProgressBar,an ImageView and a TextView,I update all three from an AsyncTask.All three get updated when the screen is completely in one orientation when the task is running,but the ImageView and TextView are not displayed and the ProgressBar freezes when the screen orientation changes from one orientation to another.
Adding the attach and detach methods to the task and using retainNonConfigurationInstance to return the task when the Activity and using getLastNonConfigurationInstance is destroyed has had no effect.I have also implement three methods for getting the various progress values from the AsyncTask to no effect.
MyActivity looks like this:
static final String TAG="ImageUpdateActivity";
TextView txt_currentOp;
ImageView img_currentOp;
ImageUpdatingTask task;
CustomProgressBar updatebar;
#SuppressWarnings("deprecation")
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_imageupdate);
txt_currentOp=(TextView)findViewById(R.id.txt_currentOp);
img_currentOp=(ImageView)findViewById(R.id.img_updateOp);
updatebar=(CustomProgressBar)findViewById(R.id.progressbar_update);
String filename=getIntent().getStringExtra("pathName");
task=(ImageUpdatingTask)getLastNonConfigurationInstance();
if(task!=null)
{
task.attach(this);
if(task.getStatus()==AsyncTask.Status.RUNNING)
{
Log.d(TAG, "The progress description is: "+task.getProgressDesc());
txt_currentOp.setText(task.getProgressDesc());
img_currentOp.setImageBitmap(task.getProgressBitmap());
updatebar.setProgress(task.getProgress());
}
}
else
{
task=new ImageUpdatingTask(this);
task.execute(filename);
}
}
public Object retainNonConfigurationInstance()
{
task.detach();
return task;
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
if(task.getStatus()!=AsyncTask.Status.FINISHED)
{
task.cancel(true);
task=null;
}
Intent i=new Intent(this,ImagePreviewActivity.class);
startActivity(i);
}
return super.onKeyDown(keyCode, event);
}
This is how I update the progress from my doInBackground method where
int progress=0;
Bitmap progressBitmap=null;
String progressDesc=null;
are global variables.
mOperation=BITMAP_TO_PIX;
progressDesc=getValueFromOperation(mOperation);
Pix pix=convertBitmapToPix(bitmap);
mOperation=CONVERT_TO_8;
progressDesc=getValueFromOperation(mOperation);
Pix pix2=convertOperation(pix);
temp=pix2.copy();
tempImg=convertPixToBitmap(temp);
progressBitmap=tempImg;
temp=null;
progress+=10;//60
publishProgress(tempImg);
And in my publishProgress I use:
#Override
protected void onProgressUpdate(Bitmap... values) {
// TODO Auto-generated method stub
super.onProgressUpdate(values);
int oldOperation=0,oldProgress=0;
if(mOperation!=oldOperation)
{
String progressText=getValueFromOperation(mOperation);
Log.d(TAG, progressText);
activity.txt_currentOp.setText(progressText);
oldOperation=mOperation;
}
if(oldProgress!=progress)
{
Log.d(TAG,"Update the progress: "+progress);
activity.updatebar.setProgress(progress);
oldProgress=progress;
}
activity.img_currentOp.setImageBitmap(values[0]);
}
And the Activity,is passed to the task using the constructor:
public ImageUpdatingTask(ImageUpdateActivity activity)
{
this.activity=activity;
}
These are the methods that take care of interaction between the AsyncTask and the Activity:
public void attach(ImageUpdateActivity activity)
{
this.activity=activity;
}
public void detach()
{
activity=null;
}
public int getProgress()
{
return progress;
}
public Bitmap getProgressBitmap()
{
return progressBitmap;
}
public String getProgressDesc()
{
return progressDesc;
}
When orientation changes your activity gets is destroyed and recreated. Fragments are hosted by an activity.
By default, Fragments are destroyed and recreated along with their parent Activitys when a configuration change occurs. Calling Fragments setRetainInstance(true) allows us to bypass this destroy-and-recreate cycle, signaling the system to retain the current instance of the fragment when the activity is recreated.
public void setRetainInstance (boolean retain)
Added in API level 11
Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. If set, the fragment lifecycle will be slightly different when an activity is recreated:
onDestroy() will not be called (but onDetach() still will be, because the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being re-created.
onAttach(Activity) and onActivityCreated(Bundle) will still be called.
You can check this blog for a workaround suggested . Uses interface as callback to the activity.
http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
and the source code for the same is available at
https://github.com/alexjlockwood/worker-fragments
Quoting from the blog
Flow of Events
When the MainActivity starts up for the first time, it instantiates and adds the TaskFragment to the Activity's state. The TaskFragment creates and executes an AsyncTask and proxies progress updates and results back to the MainActivity via the TaskCallbacks interface. When a configuration change occurs, the MainActivity goes through its normal lifecycle events, and once created the new Activity instance is passed to the onAttach(Activity) method, thus ensuring that the TaskFragment will always hold a reference to the currently displayed Activity instance even after the configuration change. The resulting design is both simple and reliable; the application framework will handle re-assigning Activity instances as they are torn down and recreated, and the TaskFragment and its AsyncTask never need to worry about the unpredictable occurrence of a configuration change.

Activity life cycle issues

I want to hide a button in one of my activities if a global data structure does not exist (it's static, in public class Globals extends Application). Since I want to redraw the button whenever I resume the activity, but would rather not redraw the rest of the view, I put the initialization of the view in onCreate() and the button-hiding code in onResume():
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.myActivity);
}
#Override
protected void onResume() {
super.onResume();
if (Globals.datastructure == null) {
((Button) findViewById(R.id.myButton)).setVisibility(View.GONE);
}
}
When I allocate the data structure and then go back to the activity from a different activity, onResume is executed correctly but the button does not reappear.
The activity containing the button is probably not being recreated, which means that when you return to it from some other activity, the button is never being set (back) to be visible. You should probably change the onResume() to something like:
#Override
protected void onResume() {
super.onResume();
findViewById(R.id.myButton).setVisibility(Globals.datastructure == null ? View.GONE : View.VISIBLE);
}
So basically you just have to make sure that whenever Globals.datastructure != null, you also change the visibility appropriately. In other words: an else is required with the if.

Categories

Resources