android on view removed from parent - android

on a child layout (View) is there a callback for when the view is removed from it's parent? I need to recycle some images when the view is done. I've been looking around on the web for what to do, but haven't found anything helpful yet.

I've been looking for something like this too. The best I can find is View.OnAttachStateChangeListener. I doubt it's ideal, as it's the callback for when the View is added & removed from the Window - not the parent, but it's sufficient for my needs.

Instead of registering a new listener, you can override onDetachedFromWindow in your custom View code.

I fall in that trap what marmor said:)
#Override
protected void onDetachedFromWindow() { I want to do something here, sometimes called sometimes not!!}
protected void onAttachedToWindow() {It is working fine, always}
This code is in a CustomView.
The calling code is:
contentHolder.removeAllViews();
// ... init my CustomView ...
contentHolder.addView(myCustomView);
contentHolder.requestLayout();// useless, not need
contentHolder.invalidate();// useless, not need
To understand why is not working you have to go inside Android API:
public void removeAllViews() {
removeAllViewsInLayout();
requestLayout();
invalidate(true);
}
public void removeAllViewsInLayout() {
final int count = mChildrenCount;
if (count <= 0) {
return;
}
final View[] children = mChildren;
mChildrenCount = 0;
final View focused = mFocused;
final boolean detach = mAttachInfo != null;
boolean clearChildFocus = false;
needGlobalAttributesUpdate(false);
for (int i = count - 1; i >= 0; i--) {
final View view = children[i];
if (mTransition != null) {
mTransition.removeChild(this, view);
}
if (view == focused) {
view.unFocus(null);
clearChildFocus = true;
}
view.clearAccessibilityFocus();
cancelTouchTarget(view);
cancelHoverTarget(view);
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
addDisappearingView(view);
} else if (detach) {
view.dispatchDetachedFromWindow();
}
if (view.hasTransientState()) {
childHasTransientStateChanged(view, false);
}
dispatchViewRemoved(view);
view.mParent = null;
children[i] = null;
}
if (clearChildFocus) {
clearChildFocus(focused);
if (!rootViewRequestFocus()) {
notifyGlobalFocusCleared(focused);
}
}
}
The key is here:
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
So, if you have animation ( and in 1 case I have and in 9 cases not) it will not called the onDetachedFromWindow() and it will mess the whole UI :)
public void endViewTransition(View view) {
if (mTransitioningViews != null) {
mTransitioningViews.remove(view);
final ArrayList<View> disappearingChildren = mDisappearingChildren;
if (disappearingChildren != null && disappearingChildren.contains(view)) {
disappearingChildren.remove(view);
if (mVisibilityChangingChildren != null &&
mVisibilityChangingChildren.contains(view)) {
mVisibilityChangingChildren.remove(view);
} else {
if (view.mAttachInfo != null) {
view.dispatchDetachedFromWindow();
}
if (view.mParent != null) {
view.mParent = null;
}
}
invalidate();
}
}
}
Again in some cases will be called even with animation.
addDisappearingView(view);
The accepted answer suggest something like this:
addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
#Override
public void onViewAttachedToWindow(View v) {
}
#Override
public void onViewDetachedFromWindow(View v) {
System.out.println("MyCustomView.onViewDetachedFromWindow");
}
});
Sadly on animation will not print the desired text.
Some important code from android.view.ViewGroup API:
void dispatchViewRemoved(View child) {
onViewRemoved(child);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(this, child);
}
}
public void onViewRemoved(View child) {
}
So, you can override your RelativeLayout for this method.
My animation is an infinite animation, and it will not be called very soon any of the methods!
If you have an infinite animation the correct way is to write this code, when you call remove all views:
if(contentHolder.getChildCount() > 0 ){
View child0 = contentHolder.getChildAt(0);
Animation animation = child0.getAnimation();
if(animation != null) {
animation.cancel();
child0.clearAnimation();
}
}
contentHolder.removeAllViews();
Now it will be called the protected void onDetachedFromWindow()!

The Android KTX (Core KTX) library gives you a nice solution for this.
You'll need this dependency: androidx.core:core-ktx:1.3.0
You can then call a function "doOnDetach" to signal you want to run some code (once) when the view is removed from the window:
fun myInitCode() {
...
myView.doOnDetach(this::doOnMyViewDetachFromWindow)
...
}
fun doOnMyViewDetachFromWindow(view: View) {
... put your image cleanup code here ...
}
You can pass a lambda to "doOnDetach" but a method reference as shown above may be cleaner, depending on how much work you have to do.
The description of doOnDetach is as follows:
androidx.core.view ViewKt.class public inline fun View.doOnDetach(
crossinline action: (View) → Unit ): Unit
Performs the given action when this view is detached from a window. If
the view is not attached to a window the action will be performed
immediately, otherwise the action will be performed after the view is
detached from its current window. The action will only be invoked
once, and any listeners will then be removed.

Related

Change image in imageview onClick

I want to change the images in image view on Onclick function. I have already tried this:
bt_audiocapture.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
if (checkPermission()) {
if (bt_audiocapture.getResources().getDrawable(R.drawable.ic_mic).equals(R.drawable.ic_mic)) {
start();
bt_audiocapture.setImageResource(R.drawable.ic_stop);
} else if (bt_audiocapture.getResources().getDrawable(R.drawable.ic_stop).equals(R.drawable.ic_stop)) {
stop();
bt_audiocapture.setImageResource(R.drawable.ic_play);
} else if (bt_audiocapture.getResources().getDrawable(R.drawable.ic_play).equals(R.drawable.ic_play)) {
play();
}
}
}
});
bt_audiocapture.getResources().getDrawable(R.drawable.ic_mic) returns a Drawable object. You can't compare it to R.drawable.ic_mic, which is integer. That is something like comparing a car to green color.
To accomplish your task, make some field like private int state = 0;, and some constants like
private final STATE_PLAYING = 1;
private final STATE_STOPPED = 2;
private final STATE_NONE = 0;
and then:
if (state == STATE_NONE) {
start();
bt_audiocapture.setImageResource(R.drawable.ic_stop);
state = STATE_PLAYING;
} else if (state == STATE_PLAYING) {
stop();
bt_audiocapture.setImageResource(R.drawable.ic_play);
state = STATE_STOPPED;
} else if (state == STOPPED) {
play();
state = STATE_PLAYING;
}
Your equals expressions always return false, I would expect that none of your if blocks is executed. Did you debug that code?
I would suggest to keep the state in another variable, e.g. with an enum.
Additionally you should use setImageDrawable for performance reasons. See the javadoc of setImageResource:
This does Bitmap reading and decoding on the UI thread, which can cause a latency hiccup
Try this code, its working for me:-
img = (ImageView) findViewById(R.id.img);
img.setTag(0);
img.setImageResource(R.drawable.images);
img.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (Integer.parseInt(img.getTag().toString()) == 0) {
img.setImageResource(R.drawable.cam);
img.setTag(1);
} else {
img.setImageResource(R.drawable.images);
img.setTag(0);
}
}
});

Appending ListView from onScrollListener

I am trying to update my listview to make an "endless scroll". What happens is that the first 40 results load fine, when i get to the bottom of the scroll, next 40 results replace the first 40...
What I want is for second set of 40 results to add to the first 40 so I have an endless list and ability to scroll back to the beginning of the list.
I am posting my code below. Thank you!
public class SearchResults extends Activity implements BannerAdListener, OnScrollListener{
private LinearLayout bottomNav;
private ListView ringtoneList;
private int start = 0, num = 40, curPage = 1;
private Handler handler = new Handler();
private ProgressDialog progressDialog = null;
private ArrayList<Ringtone> ringtones;
private MoPubView moPubView;
private String searchString;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
if (extras == null) {
// no search string defined
finish();
} else {
searchString = extras.getString("search_string");
}
setContentView(R.layout.search_results);
ringtoneList = (ListView)findViewById(R.id.ringtone_list);
ringtoneList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> av, View view, int position, long id) {
Intent i = new Intent(SearchResults.this, RingtoneView.class);
i.putExtra("ringtone", ringtones.get(position));
startActivity(i);
}
});
performSearch();
moPubView = (MoPubView) findViewById(R.id.adview);
moPubView.setAdUnitId(Utils.MoPubBannerId);
moPubView.loadAd();
moPubView.setBannerAdListener(this);
ringtoneList.setOnScrollListener(this);
}
public void onScroll(AbsListView view, int firstVisible, final int visibleCount, int totalCount) {
Log.i("List", "firstVisible="+firstVisible+" visibleCount="+visibleCount+" totalCount="+totalCount);
boolean loadMore = firstVisible + visibleCount >= totalCount;
if(loadMore) {
Log.i("List", "Loading More Results");
curPage++;
start = num * (curPage-1);
new Thread() {
public void run() {
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
handler.post(new Runnable() {
public void run() {
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
} else {
handler.post(new Runnable() {
public void run() {
new AlertDialog.Builder(SearchResults.this)
.setTitle(getResources().getString(R.string.context_info)).setMessage(getResources().getString(R.string.context_noresult))
.setPositiveButton(getResources().getString(R.string.context_ok), null).show();
}
});
}
}
}
.start();
}
}
public void onScrollStateChanged(AbsListView v, int s) { }
private void performSearch() {
progressDialog = ProgressDialog.show(SearchResults.this, getResources().getString(R.string.loading_message), getResources().getString(R.string.loading_search), true);
new Thread() {
public void run() {
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
updateList();
} else {
handler.post(new Runnable() {
public void run() {
new AlertDialog.Builder(SearchResults.this)
.setTitle(getResources().getString(R.string.context_info)).setMessage(getResources().getString(R.string.context_noresult))
.setPositiveButton(getResources().getString(R.string.context_ok), null).show();
ringtoneList.setVisibility(View.INVISIBLE);
bottomNav.setVisibility(View.INVISIBLE);
}
});
}
progressDialog.dismiss();
}
}.start();
}
private void updateList() {
handler.post(new Runnable() {
public void run() {
//Log.d("search", "ringtones.size() " + ringtones.size());
ringtoneList.setVisibility(View.VISIBLE);
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
}
}
Please help! Thank you!
While I can't be 100% sure, I think your problem has to do with the fact that you're setting a new adapter that only has the section of ringtones that in loads. It probably has to do with this snippet:
ringtones = Utils.search(start, num, searchString);
if (ringtones != null && ringtones.size() > 0) {
handler.post(new Runnable() {
public void run() {
ringtoneList.setAdapter(new RingtoneRowAdapter(SearchResults.this, ringtones));
}
});
}
Instead of putting an entirely new array of ringtones, you should add on to the one you already have. Your ringtones variable is already an instance variable, so I'm sure if you changed this line:
ringtones = Utils.search(start, num, searchString);
to the following:
ringtones.addAll(Utils.search(start, num, searchString));
It might fix your problem.
Your code is a little bit messy, but your updateList method is creating a NEW RingtoneRowAdapter. You should ADD items to the list and call
mRingtoneRowAdapter.notifyDatasetChanged();
That will tell the adapter to get new views (if needed) along with a lot of internal stuff happening at Adapter's level. So it's kinda:
private void updateList() {
handler.post(new Runnable() {
public void run() {
if ( mAdapter == null ) {
mAdapter = new RingtoneRowAdapter(SearchResults.this, ringtones);
ringtoneList.setAdapter(mAdapter);
} else {
mAdapter.notifyDataSetChanged();
}
ringtoneList.setVisibility(View.VISIBLE);
}
});
}
And of course, as already suggested, don't init your ringtones array all the time. Just add data to it.
You don't need (most of the times) to hide the list, you can add in your layout, any View (including a full Layout!) with an id of:
android:id="#android:id/empty"
And if your ListView has an id of android:id="#id/android:list", and you're in a ListFragment or ListActivity, then when the list is empty, the "empty" layout will be shown (which can be a dummy view if you don't want to see it).
Just suggestions :)
UPDATE: For your null, I see that the logic is a little bit weird and since we don't know the requirements for your app (i.e. what to do if there are no results, what to do if the results are invalid, etc.) I'll assume you just want to make sure it works and deal with that later.
So with that in mind, I see two problems.
I assume your Utils.search method can return null, because you're checking for it. To me that feels strange, I'd rather return an empty array indicating that the search produced no results; that little remark aside, you are not checking (or we don't know because we haven't seen the source for your search method), if the searchString is null or not.
You haven't provided a stack trace, so we can't tell where the null is happening (either inside search or ?)
A quick solution would be to check for null before searching… I would personally do this INSIDE the search function.
Something like:
public ArrayList<Ringtone> search(final int start, final int num, final String searchString) {
if ( searchString == null ) {
//DECIDE WHAT YOU WANT TO DO, EITHER:
return null;
// OR YOU CAN RETURN AN EMPTY ARRAY
return new ArrayList<Ringtone>();
}
// You should check for these (change according to your rules)
if (start < 0 ) {
start = 0; // protect yourself from bad data.
}
if ( num < 0 ) {
num = 0;
}
/// THE REST OF YOUR search FUNCTION
return <your array>
}
Now another thing is that you may want the search to return incremental results (so you can ADD them to your array (instead of returning a new array every time). For that, as already suggested, use the addAll trick, but then, DON'T return null, return an new empty array, so there's no harm done if there's nothing else to add.

Android Disable Multitouch on Views

I am facing multitouch issue. The problem is I can simultaneously touch two buttons on my screen. I know this question is asked several times in this forum and the only solution is to declare android:splitMotionEvents="false" in your parent layout. But after declaring this the issue remains. Is it the problem with the hardware or is it with the code ? Any pointer here is appreciated.
This issue appears beacuse since android 4.0 each onClick performed in a new thread.
How i solved it:
//1. create your own click listener
public abstract class AbstractCarOnClickListener {
protected static volatile boolean processing = false;
protected void executeBlock() {
ActivityUtil.scheduleOnMainThread(new Runnable() {
public void run() {
processing=false;
}
}, 400);
}
}
//2. create subclass of your listener
public abstract class AppButtonsOnClickListener extends AbstractCarOnClickListener implements View.OnClickListener {
public void onClick(View v) {
if(processing) return;
try{
processing=true;
onCarButtonClick(v);
} finally {
executeBlock();
}
}
public abstract void onCarButtonClick(View v);
}
//3. set listener to your view
public void onClick(View v) {
clickListener.onClick(v);
}
public OnClickListener clickListener = new AppButtonsOnClickListener(){
public void onCarButtonClick(View v) {
hintContainer.setVisibility(View.GONE);
if (v == cancelButton) {
listener.onCancelButtonClicked();
}
}
}
This is what worked for me. In addition to setting the android:splitMotionEvents="false" on every ViewGroup that contains the buttons I put this in MyAdapter.getView()...
view.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View cell, MotionEvent event) {
// Process touches only if: 1) We havent touched a cell, or 2) This event is on the touched cell
if (mTouchedCell == null || cell.equals(mTouchedCell)) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
cell.startAnimation(mShrink);
mTouchedCell = cell;
} else if (action == MotionEvent.ACTION_CANCEL) {
if (cell.equals(mTouchedCell)) {
cell.startAnimation(mGrow);
}
mTouchedCell = null;
return true;
} else if (action == MotionEvent.ACTION_UP) {
cell.startAnimation(mFadeOut);
mTouchedCell = null;
}
return false;
}
return true;
}
});
...and of course this in the adapter...
private static View mTouchedCell = null;

Extending the Drawable Class (using ProxyDrawable example)

Overview: I'm trying to make a class which extends Drawable. My test app does not behave as I expect. No image fills the screen, when I expect one should.
Details: I am trying to understand how to create custom Drawables that take other Drawables as parameters and manipulate them as desired. I've read through and generally understand the source code for obvious existing examples such as LayerDrawable and LevelListDrawable, then I came across a very stripped back version of this concept in the ProxyDrawable class.
As I understand it, it basically;
overrides Drawable's getters to take the properties of the Drawable passed to its constructor as its own.
overrides Drawable's setters to pass on properties down to the passed in Drawable.
calls the passed in Drawable's own draw() method to draw it to the ProxyDrawable's canvas.
I have tried to implement this in a simple test app, as shown in the code below.
The FrameLayout frame1 is set to match_parent for both the width and height in my activity_main.xml so it takes up the full screen.
When I run this test app however, the ic_launcher image does not appear in the frame.
As noted in the code comments, if I use the ic_launcher directly when setting the frame's background, the image does appear. Hence the problem must be in the MyProxyDrawable class.
public class MainActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyProxyDrawable myDrawable = new MyProxyDrawable(getResources().getDrawable(R.drawable.ic_launcher));
FrameLayout myFrame = (FrameLayout)findViewById(R.id.frame1);
myFrame.setBackground(myDrawable);
// ** NOTE **
// The ic_launcher image fills the whole screen if
// I replace the line above with the line below, so there
// is no problem with the resource or frame.
//myFrame.setBackground(getResources().getDrawable(R.drawable.ic_launcher));
}
public class MyProxyDrawable extends Drawable {
private Drawable mProxy;
private boolean mMutated;
public MyProxyDrawable(Drawable target) {
mProxy = target;
}
public Drawable getProxy() {
return mProxy;
}
public void setProxy(Drawable proxy) {
if (proxy != this) {
mProxy = proxy;
}
}
#Override
public void draw(Canvas canvas) {
if (mProxy != null) {
mProxy.draw(canvas);
}
}
#Override
public int getIntrinsicWidth() {
return mProxy != null ? mProxy.getIntrinsicWidth() : -1;
}
#Override
public int getIntrinsicHeight() {
return mProxy != null ? mProxy.getIntrinsicHeight() : -1;
}
#Override
public int getOpacity() {
return mProxy != null ? mProxy.getOpacity() : PixelFormat.TRANSPARENT;
}
#Override
public void setFilterBitmap(boolean filter) {
if (mProxy != null) {
mProxy.setFilterBitmap(filter);
}
}
#Override
public void setDither(boolean dither) {
if (mProxy != null) {
mProxy.setDither(dither);
}
}
#Override
public void setColorFilter(ColorFilter colorFilter) {
if (mProxy != null) {
mProxy.setColorFilter(colorFilter);
}
}
#Override
public void setAlpha(int alpha) {
if (mProxy != null) {
mProxy.setAlpha(alpha);
}
}
#Override
public Drawable mutate() {
if (mProxy != null && !mMutated && super.mutate() == this) {
mProxy.mutate();
mMutated = true;
}
return this;
}
}
}

Detect splash screen exit on Unity Android/Eclipse project

I've created an Eclipse project based on a Vuforia/Unity project by following the instructions here. That's up and running.
I am adding a button to my main activity, extends from QCARPlayerActivity. That also works, however, the button sits on top of the Unity player as the Unity splash screen plays.
Is there any way to detect when the Unity splash screen exits so I don't have controls in place before the scene loads?
UPDATE 3/18/13
I've added a static boolean to my main activity in Eclipse to track splash screen completion and modified the code that adds the controls to watch the boolean.
MainActivity.java
public static boolean splashComplete = false;
private View mControlViewContainer = null; // initialized in onCreate
class QCARViewFinderTask extends TimerTask {
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
public void run() {
if (!QCAR.isInitialized()) return; //wait for QCAR init
//search for QCAR view if it hasn't been found
if (mQCARView == null)
{
View rootView = MainActivity.this.findViewById(android.R.id.content);
QCARUnityPlayer qcarView = findQCARView(rootView);
if (qcarView != null) {
mQCARParentView = (ViewGroup)(qcarView.getParent());
mQCARView = qcarView;
}
}
// add controls if QCAR view is located and the splash sequence is complete
if(mQCARView != null && splashComplete && mControlViewContainer != null){
mQCARParentView.addView(mControlViewContainer);
mViewFinderTimer.cancel();
mViewFinderTimer = null;
}
}
});
}
}
In Unity I created a simple script to set the static boolean in Java and attached it to the Vuforia ARCamera
SplashExit.js
function Start () {
var mainActivity = new AndroidJavaClass ("com.example.app.MainActivity");
mainActivity.SetStatic.("splashComplete",true);
}
This works fairly well in a project with a simple scene. My controls seem to load on splash exit. When I use this method with a more complicated scene, however, the controls come up a second or so before the splash screen disappears.
Is there a better place to attach my Unity script, or a better method within the script, that will more accurately reflect when the splash sequence has exited? Perhaps Jerdak's suggestion in the comments?
Adding a yield statement did the trick. Full solution follows.
SplashExit.js should be attached to the ARCamera Game object in Unity. The start method will stall until the scene has loaded up, then set splashComplete to true in MainActivity.java.
As the timer in MainActivity.java repeatedly calls the run method of QCARViewFinderTask, the control view will be added to the Unity Player parent view as splashComplete transitions to true.
MainActivity.java
public class MainActivity extends QCARPlayerActivity {
private QCARUnityPlayer mQCARView = null;
private ViewGroup mQCARParentView = null;
private Timer mViewFinderTimer = null;
private View mControlViewContainer = null;
public static boolean splashComplete = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mControlViewContainer = getLayoutInflater().inflate(R.layout.control_layout, null);
}
#Override
public void onResume() {
super.onResume();
if (mQCARView == null) {
//search the QCAR view
mViewFinderTimer = new Timer();
mViewFinderTimer.scheduleAtFixedRate(new QCARViewFinderTask(), 1000, 1000);
}
}
#Override
public void onPause() {
super.onPause();
if (mViewFinderTimer != null) {
mViewFinderTimer.cancel();
mViewFinderTimer = null;
}
}
class QCARViewFinderTask extends TimerTask {
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
public void run() {
if (!QCAR.isInitialized()) return; //wait for QCAR init
//search for QCAR view if it hasn't been found
if (mQCARView == null)
{
View rootView = MainActivity.this.findViewById(android.R.id.content);
QCARUnityPlayer qcarView = findQCARView(rootView);
if (qcarView != null) {
mQCARParentView = (ViewGroup)(qcarView.getParent());
mQCARView = qcarView;
}
}
// add controls if QCAR view is located and the splash sequence is complete
if(mQCARView != null && splashComplete && mControlViewContainer != null){
mQCARParentView.addView(mControlViewContainer);
mViewFinderTimer.cancel();
mViewFinderTimer = null;
}
}
});
}
private QCARUnityPlayer findQCARView(View view) {
if (view instanceof QCARUnityPlayer) {
return (QCARUnityPlayer)view;
}
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup)view;
for (int i = 0; i
SplashExit.js
function Start () {
yield; // wait for the scene to fully load
// Note that com.example.app.MainActivity should be updated to match your bundle identifier and class names
var mainActivity = new AndroidJavaClass ("com.example.app.MainActivity");
mainActivity.SetStatic.("splashComplete",true);
}

Categories

Resources