Android invalidate(Rect) invalidates entire region - android

I don't understand why invalidate(Rect) is invalidating the entire region.
The region is divided up into 5 sections and a line graph is being drawn in each one. Each section contains 100 points. As the data arrives for times tn to tn+100 I call invalidate(new Rect(left, top, right bottom)) where top is the top of the screen in height (but a lower numerical value than bottom). This invokes a call to the onDraw() method. The region from tn to tn+100 is drawn, but the previously drawn segment in region tn-100 to tn is erased. It continues that way forever. Each invalidate draws in only that region (since that is the only data I have) which IS correct, but all the previously drawn data is erased. So I get a marching segment!
In other words, I get identical behavior if I call invalidate() or invalidate(Rect).
I am assuming that the parameters of the Rect() are pixels and are getting the values based upon the height and width of the AlertDialog window in which this is being drawn.
The hope is eventually to reduce the region of 'Rect()' so I can simulate real time drawing and only invalidate time step t to t+1 instead of a region.
I must be doing something stupid.
I hope that the fact it is being done in an AlertDialog is not the issue.
This part is for trying to help 'android developer' help a noob like me get this right.
First the sequence of events:
1. Data is received via Bluetooth in a callback
2. If it is the right type of data, a BroadcastReceiver in the main activity (UI thread) is signaled and from there a routine is called that sets the parameters of a WaveFormView extends View class and then ShowDialog(id) is called which calls the onCreateDialog(id) callback in the main activity.
3. Then I call invalidate().
4. The dialog pops up and then the graph is drawn.
5. All subsequent calls to ShowDialog(id) bypass the onCreateDialog(id)
That works but the entire region is always invalidated regardless of the parameters. There are also no user events here. From your example the best I could come up with is the following where I place invalidate in the onShow() instead of calling myself after the showDialog(id)
#Override
protected Dialog onCreateDialog(int id)
{
Log.d(TAG, "Alert Dialog 'onCreateDialog' method has been called with id " + id);
Builder bldr = new AlertDialog.Builder(this);
AlertDialog alert = bldr.setView(waveForm).setNegativeButton("Dismiss " + id,
new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int id)
{
dialog.cancel();
}
}).create();
// I tried adding this and invalidating here worked only first pass
alert.setOnShowListener(
new DialogInterface.OnShowListener()
{
#Override
public void onShow(DialogInterface dialog)
{
// call to invalidate
waveForm.drawArea();
}
});
//alert.getWindow().setLayout(alert.getWindow().getAttributes().width, alert.getWindow().getAttributes().height / 2);
alert.getWindow().setLayout(waveForm.getCurrentWidth(), waveForm.getCurrentHeight());
return alert;
}
However the onShow() does not get called.
The method in the main activity that calls the showDialog is
private void displayRtsa(int[] rtsaReceived)
{
// rtsaReceived[0] has the agentsink hash code
int agent = rtsaReceived[0];
// rtsaReceived[1] has the index of the RtsaData object updated
int index = rtsaReceived[1];
TreeMap<Integer, RtsaData[]> agentRtsa = BluetoothPanService.getAgentRtsaMap();
RtsaData[] rtsaDataValues = agentRtsa.get(agent);
int dialogId = 0;
synchronized(dialogIds)
{
int i = 0;
if(dialogIds.containsKey(agent + index) == false)
{
for(i = 0; i < dialogIds.size(); i++)
{
if(dialogIds.containsValue(i) == false)
{
break;
}
}
dialogIds.put(agent + index, i);
}
dialogId = dialogIds.get(agent + index);
}
final int id = dialogId;
Log.d(TAG, "Dialog id being shown = " + dialogId);
waveForm.setPeriod(rtsaDataValues[index].getPeriod());
waveForm.setMaxMin(rtsaDataValues[index].getMinValue(), rtsaDataValues[index].getMaxValue());
waveForm.setColor(Color.argb(255, 255, 200, 0));
waveForm.setData(rtsaDataValues[index].getData());
waveForm.setTitle(rtsaDataValues[index].getType());
showDialog(id);
// invalidate
// waveForm.drawArea(); (try to do in onCreateDialog callback)
}
This is probably a completely wrong approach. Probably openGl is the only way.
By the way, thanks for putting up with me!

i think it depends on what exactly you do the invalidation on . if the view you are calling the invalidation on didn't handle a rectangular invalidation , the default invalidation takes place.
in any case , if you wish to change the behavior , you can change it yourself. for the "onDraw" method , use the next code in order to fetch the invalidated rectangle :
public class InvalidateTestActivity extends Activity
{
static class CustomView extends ImageView
{
public CustomView(Context context)
{
super(context);
}
#Override
protected void onDraw(Canvas canvas)
{
final Rect r = canvas.getClipBounds();
Log.d("DEBUG", "rectangle of invalidation:" + r);
super.onDraw(canvas);
}
}
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
CustomView customView = new CustomView(this);
customView.setLayoutParams(new FrameLayout.LayoutParams(200, 200));
customView.setBackgroundColor(0xffff0000);
customView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v)
{
v.invalidate(new Rect(0, 0, 49, 49));
}
});
setContentView(customView);
}
}

If you have hardware acceleration enabled it looks like it just invalidates the whole view...
ViewGroup:
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}

Related

RecyclerView - ItemAnimator -> animateChange does not work properly?

I am trying to get my head around the ItemAnimator class to be used if I want to animate the content of my RecyclerView.
Here my problem is very specific : I don't understand how the change animation works and I am not using move / add / delete animations: only change.
I've been looking to examples like the BaseItemAnimator of this project to see how it uses the SimpleItemAnimator as defined inside the android support library.
When the change animation is actually run after the method runPendingAnimation() is called I can see that there is two ViewHolders containing the same view but not exactly (I checked, different IDs, but SAME content!) and both are animated with translation and alpha values.
I tried adding my own animation, like scale to see if I get it, but no because some time it works some time it does not. I just don't understand how the RecyclerView uses his view holders when there are such animations.
Can anyone tell me how I should be using these view holders so that my own animations are played properly everytime ? I'd like to run an animation that resize the cell.
Thanks !
PS:
I use the Adapater#notifyItemChange(position) method to trigger the animation
I trigger the animation when the RecyclerView is not being scrolled
I use setSupportsChangeAnimations(true) so that change animations are triggered
EDIT 1
I'd like to show the code where I attempt to add my own animations alongside the the already existing animations. Remember, we are inside runPendingChanges().
if (changesPending) {
final List<CustomChangeInfo> changes = new ArrayList<>(this.mPendingChangesCustom.size());
changes.addAll(this.mPendingChangesCustom);
this.mPendingChangesCustom.clear();
final Runnable changer = new Runnable() {
#Override
public void run() {
for (final CustomChangeInfo customChangeInfo : changes) {
final ChangeInfo changeInfo = customChangeInfo.changeInfo;
final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
final View oldView = holder == null ? null : holder.itemView;
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (oldView != null) {
mChangeAnimations.add(changeInfo.oldHolder);
final ViewPropertyAnimatorCompat oldViewAnim =
ViewCompat.animate(oldView).setDuration(2500);
oldViewAnim.scaleX(1.5f).scaleY(1.5f);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(1).setListener(new VpaListenerAdapter() {
#Override public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.oldHolder, true);
Log.d(TAG, "Start Old View id = " + view.getId());
}
#Override public void onAnimationEnd(View view) {
Log.d(TAG, "End Old View id = " + oldView.getId());
oldViewAnim.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
ViewCompat.setTranslationY(view, 0);
ViewCompat.setScaleX(view, 1f);
ViewCompat.setScaleY(view, 1f);
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
if (newView != null) {
mChangeAnimations.add(changeInfo.newHolder);
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
newViewAnimation.translationX(0).translationY(0).setDuration(2500)
.scaleX(1.5f).scaleY(1.5f)
.alpha(1).setListener(new VpaListenerAdapter() {
#Override public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.newHolder, false);
Log.d(TAG, "Start New View id = " + view.getId());
}
#Override public void onAnimationEnd(View view) {
Log.d(TAG, "End New View id = " + newView.getId());
newViewAnimation.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationX(newView, 0);
ViewCompat.setTranslationY(newView, 0);
ViewCompat.setScaleX(newView, 1f);
ViewCompat.setScaleY(newView, 1f);
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
}
};
As you see I added some log to check the IDs to see if we're working on different views here and it is indeed the case !
The results are inconsistent :
1. some time I can see two views scaling, sometime only one
2. some time it's another cell that was scaled (when I scroll after the animations)
3. some time no animations are run...

Drawing plot in real time using Android custom view

I want to make in my app simple line plot with real time drawing. I know there are a lot of various libraries but they are too big or don't have right features or licence.
My idea is to make custom view and just extend View class. Using OpenGL in this case would be like shooting to a duck with a canon. I already have view that is drawing static data - that is first I am putting all data in float array of my Plot object and then using loop draw everything in onDraw() method of PlotView class.
I also have a thread that will provide new data to my plot. But the problem now is how to draw it while new data are added. The first thought was to simply add new point and draw. Add another and again. But I am not sure what will happen at 100 or 1000 points. I am adding new point, ask view to invalidate itself but still some points aren't drawn. In this case even using some queue might be difficult because the onDraw() will start from the beginning again so the number of queue elements will just increase.
What would you recommend to achieve this goal?
This should do the trick.
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.AttributeSet;
import android.view.View;
import java.io.Serializable;
public class MainActivity
extends AppCompatActivity
{
private static final String STATE_PLOT = "statePlot";
private MockDataGenerator mMockDataGenerator;
private Plot mPlot;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if(savedInstanceState == null){
mPlot = new Plot(100, -1.5f, 1.5f);
}else{
mPlot = (Plot) savedInstanceState.getSerializable(STATE_PLOT);
}
PlotView plotView = new PlotView(this);
plotView.setPlot(mPlot);
setContentView(plotView);
}
#Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putSerializable(STATE_PLOT, mPlot);
}
#Override
protected void onResume()
{
super.onResume();
mMockDataGenerator = new MockDataGenerator(mPlot);
mMockDataGenerator.start();
}
#Override
protected void onPause()
{
super.onPause();
mMockDataGenerator.quit();
}
public static class MockDataGenerator
extends Thread
{
private final Plot mPlot;
public MockDataGenerator(Plot plot)
{
super(MockDataGenerator.class.getSimpleName());
mPlot = plot;
}
#Override
public void run()
{
try{
float val = 0;
while(!isInterrupted()){
mPlot.add((float) Math.sin(val += 0.16f));
Thread.sleep(1000 / 30);
}
}
catch(InterruptedException e){
//
}
}
public void quit()
{
try{
interrupt();
join();
}
catch(InterruptedException e){
//
}
}
}
public static class PlotView extends View
implements Plot.OnPlotDataChanged
{
private Paint mLinePaint;
private Plot mPlot;
public PlotView(Context context)
{
this(context, null);
}
public PlotView(Context context, AttributeSet attrs)
{
super(context, attrs);
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setStrokeJoin(Paint.Join.ROUND);
mLinePaint.setStrokeCap(Paint.Cap.ROUND);
mLinePaint.setStrokeWidth(context.getResources()
.getDisplayMetrics().density * 2.0f);
mLinePaint.setColor(0xFF568607);
setBackgroundColor(0xFF8DBF45);
}
public void setPlot(Plot plot)
{
if(mPlot != null){
mPlot.setOnPlotDataChanged(null);
}
mPlot = plot;
if(plot != null){
plot.setOnPlotDataChanged(this);
}
onPlotDataChanged();
}
public Plot getPlot()
{
return mPlot;
}
public Paint getLinePaint()
{
return mLinePaint;
}
#Override
public void onPlotDataChanged()
{
ViewCompat.postInvalidateOnAnimation(this);
}
#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
final Plot plot = mPlot;
if(plot == null){
return;
}
final int height = getHeight();
final float[] data = plot.getData();
final float unitHeight = height / plot.getRange();
final float midHeight = height / 2.0f;
final float unitWidth = (float) getWidth() / data.length;
float lastX = -unitWidth, lastY = 0, currentX, currentY;
for(int i = 0; i < data.length; i++){
currentX = lastX + unitWidth;
currentY = unitHeight * data[i] + midHeight;
canvas.drawLine(lastX, lastY, currentX, currentY, mLinePaint);
lastX = currentX;
lastY = currentY;
}
}
}
public static class Plot
implements Serializable
{
private final float[] mData;
private final float mMin;
private final float mMax;
private transient OnPlotDataChanged mOnPlotDataChanged;
public Plot(int size, float min, float max)
{
mData = new float[size];
mMin = min;
mMax = max;
}
public void setOnPlotDataChanged(OnPlotDataChanged onPlotDataChanged)
{
mOnPlotDataChanged = onPlotDataChanged;
}
public void add(float value)
{
System.arraycopy(mData, 1, mData, 0, mData.length - 1);
mData[mData.length - 1] = value;
if(mOnPlotDataChanged != null){
mOnPlotDataChanged.onPlotDataChanged();
}
}
public float[] getData()
{
return mData;
}
public float getMin()
{
return mMin;
}
public float getMax()
{
return mMax;
}
public float getRange()
{
return (mMax - mMin);
}
public interface OnPlotDataChanged
{
void onPlotDataChanged();
}
}
}
Let me try to sketch out the problem a bit more.
You've got a surface that you can draw points and lines to, and you know how to make it look how you want it to look.
You have a data source that provides points to draw, and that data source is changed on the fly.
You want the surface to accurately reflect the incoming data as closely as possible.
The first question is--what about your situation is slow? Do you know where your delays are coming from? First, be sure you have a problem to solve; second, be sure you know where your problem is coming from.
Let's say your problem is in the size of the data as you imply. How to address this is a complex question. It depends on properties of the data being graphed--what invariants you can assume and so forth. You've talked about storing data in a float[], so I'm going to assume that you've got a fixed number of data points which change in value. I'm also going to assume that by '100 or 1000' what you meant was 'lots and lots', because frankly 1000 floats is just not a lot of data.
When you have a really big array to draw, your performance limit is going to eventually come from looping over the array. Your performance enhancement then is going to be reducing how much of the array you're looping over. This is where the properties of the data come into play.
One way to reduce the volume of the redraw operation is to keep a 'dirty list' which acts like a Queue<Int>. Every time a cell in your array changes, you enqueue that array index, marking it as 'dirty'. Every time your draw method comes back around, dequeue a fixed number of entries in the dirty list and update only the chunk of your rendered image corresponding to those entries--you'll probably have to do some scaling and/or anti-aliasing or something because with that many data points, you've probably got more data than screen pixels. the number of entries you redraw in any given frame update should be bounded by your desired framerate--you can make this adaptive, based on a metric of how long previous draw operations took and how deep the dirty list is getting, to maintain a good balance between frame rate and visible data age.
This is particularly suitable if you're trying to draw all of the data on the screen at once. If you're only viewing a chunk of the data (like in a scrollable view), and there's some kind of correspondence between array positions and window size, then you can 'window' the data--in each draw call, only consider the subset of data that is actually on the screen. If you've also got a 'zoom' thing going on, you can mix the two methods--this can get complicated.
If your data is windowed such that the value in each array element is what determines whether the data point is on or off the screen, consider using a sorted list of pairs where the sort key is the value. This will let you perform the windowing optimization outlined above in this situation. If the windowing is taking place in both dimensions, you most likely will only need to perform one or the other optimization, but there are two dimensional range query structures that can give you this as well.
Let's say my assumption about a fixed data size was wrong; instead you're adding data to the end of the list, but existing data points don't change. In this case you're probably better off with a linked Queue-like structure that drops old data points rather than an array, because growing your array will tend to introduce stutter in the application unnecessarily.
In this case your optimization is to pre-draw into a buffer that follows your queue along--as new elements enter the queue, shift the whole buffer to the left and draw just the region containing the new elements.
If it's the /rate/ of data entry that's the problem, then use a queued structure and skip elements--either collapse them as they're added to the queue, store/draw every nth element, or something similar.
If instead it's the rendering process that is taking up all of your time, consider rendering on a background thread and storing the rendered image. This will let you take as much time as you want doing the redraw--the framerate within the chart itself will drop but not your overall application responsiveness.
What I did in a similar situation is to create a custom class, let's call it "MyView" that extends View and add it to my layout XML.
public class MyView extends View {
...
}
In layout:
<?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">
<com.yadayada.MyView
android:id="#+id/paintme"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
Within MyView, override method "onDraw(Canvas canv)". onDraw gets a canvas you can draw on. In onDraw, get a Paint object new Paint() and set it up as you like. Then you can use all the Canvas drawing function, e.g., drawLine, drawPath, drawBitmap, drawText and tons more.
As far as performance concerns, I suggest you batch-modify your underlying data and then invalidate the view. I think you must live with full re-draws. But if a human is watching it, updating more than every second or so is probably not profitable. The Canvas drawing methods are blazingly fast.
Now I would suggest you the GraphView Library. It is open source, don't worry about the license and it's not that big either (<64kB). You can clean up the necessary files if you wish to.
You can find a sample of usages for real time plots
in the official samples - https://github.com/jjoe64/GraphView-Demos/
Balanduino project - https://github.com/TKJElectronics/BalanduinoAndroidApp/blob/master/src/com/tkjelectronics/balanduino/GraphFragment.java
From the official samples:
public class RealtimeUpdates extends Fragment {
private final Handler mHandler = new Handler();
private Runnable mTimer1;
private Runnable mTimer2;
private LineGraphSeries<DataPoint> mSeries1;
private LineGraphSeries<DataPoint> mSeries2;
private double graph2LastXValue = 5d;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_main2, container, false);
GraphView graph = (GraphView) rootView.findViewById(R.id.graph);
mSeries1 = new LineGraphSeries<DataPoint>(generateData());
graph.addSeries(mSeries1);
GraphView graph2 = (GraphView) rootView.findViewById(R.id.graph2);
mSeries2 = new LineGraphSeries<DataPoint>();
graph2.addSeries(mSeries2);
graph2.getViewport().setXAxisBoundsManual(true);
graph2.getViewport().setMinX(0);
graph2.getViewport().setMaxX(40);
return rootView;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
((MainActivity) activity).onSectionAttached(
getArguments().getInt(MainActivity.ARG_SECTION_NUMBER));
}
#Override
public void onResume() {
super.onResume();
mTimer1 = new Runnable() {
#Override
public void run() {
mSeries1.resetData(generateData());
mHandler.postDelayed(this, 300);
}
};
mHandler.postDelayed(mTimer1, 300);
mTimer2 = new Runnable() {
#Override
public void run() {
graph2LastXValue += 1d;
mSeries2.appendData(new DataPoint(graph2LastXValue, getRandom()), true, 40);
mHandler.postDelayed(this, 200);
}
};
mHandler.postDelayed(mTimer2, 1000);
}
#Override
public void onPause() {
mHandler.removeCallbacks(mTimer1);
mHandler.removeCallbacks(mTimer2);
super.onPause();
}
private DataPoint[] generateData() {
int count = 30;
DataPoint[] values = new DataPoint[count];
for (int i=0; i<count; i++) {
double x = i;
double f = mRand.nextDouble()*0.15+0.3;
double y = Math.sin(i*f+2) + mRand.nextDouble()*0.3;
DataPoint v = new DataPoint(x, y);
values[i] = v;
}
return values;
}
double mLastRandom = 2;
Random mRand = new Random();
private double getRandom() {
return mLastRandom += mRand.nextDouble()*0.5 - 0.25;
}
}
If you already have a view that draws static data then you're close to your goal.
The only thing you then have to do is:
1) Extract the logic that retrieves data
2) Extract the logic that draws this data to screen
3) Within the onDraw() method, first call 1) - then call 2) - then call invalidate() at the end of your onDraw()-method - as this will trigger a new draw and view will update itself with the new data.
I am not sure what will happen at 100 or 1000 points
Nothing, you do not need to worry about it. There are already a lot of points being plot every time there is any activity on the screen.
The first thought was to simply add new point and draw. Add another and again.
This is the way to go I feel. You may want to take a more systematic approach with this:
check if the plot will be on the screen or off the screen.
if they will be on the the screen simply add it to your array and that should be good.
if the data is not on the screen, make appropriate coordinate calculations considering the following: remove the first element from the array, add the new element in the array and redraw.
After this postinvalidate on your view.
I am adding new point, ask view to invalidate itself but still some points aren't drawn.
Probably your points are going off the screen. Do check.
In this case even using some queue might be difficult because the onDraw() will start from the beginning again so the number of queue elements will just increase.
This should not be a problem as the points on the screen will be limited, so the queue will hold only so many points, as previous points will be deleted.
Hope this approach helps.

Custom circular reveal transition results in "java.lang.UnsupportedOperationException" when paused?

I created a custom circular reveal transition to use as part of an Activity's enter transition (specifically, I am setting the transition as the window's enter transition by calling Window#setEnterTransition()):
public class CircularRevealTransition extends Visibility {
private final Rect mStartBounds = new Rect();
/**
* Use the view's location as the circular reveal's starting position.
*/
public CircularRevealTransition(View v) {
int[] loc = new int[2];
v.getLocationInWindow(loc);
mStartBounds.set(loc[0], loc[1], loc[0] + v.getWidth(), loc[1] + v.getHeight());
}
#Override
public Animator onAppear(ViewGroup sceneRoot, final View v, TransitionValues startValues, TransitionValues endValues) {
if (endValues == null) {
return null;
}
int halfWidth = v.getWidth() / 2;
int halfHeight = v.getHeight() / 2;
float startX = mStartBounds.left + mStartBounds.width() / 2 - halfWidth;
float startY = mStartBounds.top + mStartBounds.height() / 2 - halfHeight;
float endX = v.getTranslationX();
float endY = v.getTranslationY();
v.setTranslationX(startX);
v.setTranslationY(startY);
// Create a circular reveal animator to play behind a shared
// element during the Activity Transition.
Animator revealAnimator = ViewAnimationUtils.createCircularReveal(v, halfWidth, halfHeight, 0f,
FloatMath.sqrt(halfWidth * halfHeight + halfHeight * halfHeight));
revealAnimator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
// Set the view's visibility to VISIBLE to prevent the
// reveal from "blinking" at the end of the animation.
v.setVisibility(View.VISIBLE);
}
});
// Translate the circular reveal into place as it animates.
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("translationX", startX, endX);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("translationY", startY, endY);
Animator translationAnimator = ObjectAnimator.ofPropertyValuesHolder(v, pvhX, pvhY);
AnimatorSet anim = new AnimatorSet();
anim.setInterpolator(getInterpolator());
anim.playTogether(revealAnimator, translationAnimator);
return anim;
}
}
This works OK normally. However, when I click the "back button" in the middle of the transition, I get the following exception:
Process: com.adp.activity.transitions, PID: 13800
java.lang.UnsupportedOperationException
at android.view.RenderNodeAnimator.pause(RenderNodeAnimator.java:251)
at android.animation.AnimatorSet.pause(AnimatorSet.java:472)
at android.transition.Transition.pause(Transition.java:1671)
at android.transition.TransitionSet.pause(TransitionSet.java:483)
at android.app.ActivityTransitionState.startExitBackTransition(ActivityTransitionState.java:269)
at android.app.Activity.finishAfterTransition(Activity.java:4672)
at com.adp.activity.transitions.DetailsActivity.finishAfterTransition(DetailsActivity.java:167)
at android.app.Activity.onBackPressed(Activity.java:2480)
Is there any specific reason why I am getting this error? How should it be avoided?
You will need to create a subclass of Animator that ignores calls to pause() and resume() in order to avoid this exception.
For more details, I just finished a post about this topic below:
Part 1: http://halfthought.wordpress.com/2014/11/07/reveal-transition/
Part 2: https://halfthought.wordpress.com/2014/12/02/reveal-activity-transitions/
Is there any specific reason why I am getting this error?
ViewAnimationUtils.createCircularReveal is a shortcut for creating a new RevealAnimator, which is a subclass of RenderNodeAnimator. By default, RenderNodeAnimator.pause throws an UnsupportedOperationException. You see this occur here in your stack trace:
java.lang.UnsupportedOperationException
at android.view.RenderNodeAnimator.pause(RenderNodeAnimator.java:251)
When Activity.onBackPressed is called in Lollipop, it makes a new call to Activity.finishAfterTransition, which eventually makes a call back to Animator.pause in Transition.pause(android.view.View), which is when your UnsupportedOperationException is finally thrown.
The reason it isn't thrown when using the "back" button after the transition is complete, is due to how the EnterTransitionCoordinator handles the entering Transition once it's completed.
How should it be avoided?
I suppose you have a couple of options, but neither are really ideal:
Option 1
Attach a TransitionListener when you call Window.setEnterTransition so you can monitor when to invoke the "back" button. So, something like:
public class YourActivity extends Activity {
/** True if the current window transition is animating, false otherwise */
private boolean mIsAnimating = true;
#Override
protected void onCreate(Bundle savedInstanceState) {
// Get the Window and enable Activity transitions
final Window window = getWindow();
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
// Call through to super
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_child);
// Set the window transition and attach our listener
final Transition circularReveal = new CircularRevealTransition(yourView);
window.setEnterTransition(circularReveal.addListener(new TransitionListenerAdapter() {
#Override
public void onTransitionEnd(Transition transition) {
super.onTransitionEnd(transition);
mIsAnimating = false;
}
}));
// Restore the transition state if available
if (savedInstanceState != null) {
mIsAnimating = savedInstanceState.getBoolean("key");
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save the current transition state
outState.putBoolean("key", mIsAnimating);
}
#Override
public void onBackPressed() {
if (!mIsAnimating) {
super.onBackPressed();
}
}
}
Option 2
Use reflection to call ActivityTransitionState.clear, which will stop Transition.pause(android.view.View) from being called in ActivityTransitionState.startExitBackTransition.
#Override
public void onBackPressed() {
if (!mIsAnimating) {
super.onBackPressed();
} else {
clearTransitionState();
super.onBackPressed();
}
}
private void clearTransitionState() {
try {
// Get the ActivityTransitionState Field
final Field atsf = Activity.class.getDeclaredField("mActivityTransitionState");
atsf.setAccessible(true);
// Get the ActivityTransitionState
final Object ats = atsf.get(this);
// Invoke the ActivityTransitionState.clear Method
final Method clear = ats.getClass().getDeclaredMethod("clear", (Class[]) null);
clear.invoke(ats);
} catch (final Exception ignored) {
// Nothing to do
}
}
Obviously each has drawbacks. Option 1 basically disables the "back" button until the transition is complete. Option 2 allows you to interrupt using the "back" button, but clears the transition state and uses reflection.
Here's a gfy of the results. You can see how it completely transitions from "A" to "M" and back again, then the "back" button interrupts the transition and goes back to "A". That'll make more sense if you watch it.
At any rate, I hope that helps you out some.
You can add listener to enter transition that sets flag transitionInProgress in methods onTransitionStart() / onTransitionEnd(). Then, you can override method finishAfterTransition() and then check transitionInProgress flag, and call super only if transition finished. Otherwise you can just finish() your Activity or do nothing.
override fun finishAfterTransition() {
if (!transitionInProgress){
super.finishAfterTransition()
} else {
finish()
}
}

Android: Change APIDemo example AnimateDrawable.java to have a ClickEvent-Handler

I love the API Demo examples from the Android webpage and used the AnimateDrawable.java to
get started with a WaterfallView with several straight falling images which works great. Now I like the images to stop when they are clicked. I found out that Drawables can't handle events so I changed AnimateDrawable and ProxyDrawable to be extended from View instead and added a Click-Event-Listener and Handler on the parent WaterfallView. The animation still works great, but the handler doesn't, probably because in AnimateDrawable the whole canvas is shifted when the drawabled are animated. How can I change that example so that I can implement an event handler? Or is there a way to find out where exactly my AnimateDrawables are in the view?
So the more general question is: How to add an Event Listener / Handler to an animated View?
Here are my changes to the example above:
AnimateView and ProxyView instead of AnimateDrawable and ProxyDrawable
ProxyView extended from View and all super calls changed to mProxy
I commented out mutate()
The context is still the main Activity which is passed down in the constructors
In the constructors of AnimateView setClickable(true) and setFocusable(true) are called
And here is the important source code of the parent/main WaterfallView:
public class WaterfallView extends View implements OnClickListener {
private Context mContext;
// PictureEntry is just a value object to manage the pictures
private Vector<PictureEntry> pictures = new Vector<PictureEntry>();
public WaterfallView(Context context) {
super(context);
mContext = context;
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_0)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_1)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_2)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_3)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_4)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_5)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_6)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_7)));
}
#Override
protected void onDraw(Canvas canvas) {
if(!setup) {
for(PictureEntry pic : pictures) pic.setAnimation(createAnimation(pic));
setup = true;
}
canvas.drawColor(Color.BLACK);
for(PictureEntry pic : pictures) pic.getAnimateView().draw(canvas);
invalidate();
}
private Animation createAnimation(PictureEntry picture) {
Drawable dr = picture.getDrawable();
dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
Animation an = new TranslateAnimation(0, 0, -1*dr.getIntrinsicHeight(), this.getHeight());
an.setRepeatCount(-1);
an.initialize(10, 10, 10, 10);
AnimateView av = new AnimateView(mContext, dr, an);
av.setOnClickListener(this);
picture.setAnimateView(av);
an.startNow();
return an;
}
#Override
public void onClick(View v) {
Log.i("MyLog", "clicked "+v);
}
}
Are you going to be clicking widgets(buttons, checkboxes, etc)? Or do you want to be able to click anywhere? I think you want the latter. So in that case you'll need this method:
#Override
public boolean onTouchEvent(MotionEvent event) {
// do stuff with your event
}
This method is ONLY called when the event is NOT handled by a view, so I think you may have to remove some of your onClickListener stuff. Refer to here for more info. And as always, experiment.

What's the best way to check if the view is visible on the window?

What's the best way to check if the view is visible on the window?
I have a CustomView which is part of my SDK and anybody can add CustomView to their layouts. My CustomView is taking some actions when it is visible to the user periodically. So if view becomes invisible to the user then it needs to stop the timer and when it becomes visible again it should restart its course.
But unfortunately there is no certain way of checking if my CustomView becomes visible or invisible to the user. There are few things that I can check and listen to: onVisibilityChange //it is for view's visibility change, and is introduced in new API 8 version so has backward compatibility issue
onWindowVisibilityChange //but my CustomView can be part of a ViewFlipper's Views so it can pose issues
onDetachedFromWindows //this not as useful
onWindowFocusChanged //Again my CustomView can be part of ViewFlipper's views. So if anybody has faced this kind of issues please throw some light.
In my case the following code works the best to listen if the View is visible or not:
#Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
Log.e(TAG, "is view visible?: " + (visibility == View.VISIBLE));
}
onDraw() is called each time the view needs to be drawn. When the view is off screen then onDraw() is never called. When a tiny bit of the view is becomes visible to the user then onDraw() is called. This is not ideal but I cannot see another call to use as I want to do the same thing. Remember to call the super.onDraw or the view won't get drawn. Be careful of changing anything in onDraw that causes the view to be invalidate as that will cause another call to onDraw.
If you are using a listview then getView can be used whenever your listview becomes shown to the user.
obviously the activity onPause() is called all your views are all covered up and are not visible to the user. perhaps calling invalidate() on the parent and if ondraw() is not called then it is not visible.
This is a method that I have used quite a bit in my apps and have had work out quite well for me:
static private int screenW = 0, screenH = 0;
#SuppressWarnings("deprecation") static public boolean onScreen(View view) {
int coordinates[] = { -1, -1 };
view.getLocationOnScreen(coordinates);
// Check if view is outside left or top
if (coordinates[0] + view.getWidth() < 0) return false;
if (coordinates[1] + view.getHeight() < 0) return false;
// Lazy get screen size. Only the first time.
if (screenW == 0 || screenH == 0) {
if (MyApplication.getSharedContext() == null) return false;
Display display = ((WindowManager)MyApplication.getSharedContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
try {
Point screenSize = new Point();
display.getSize(screenSize); // Only available on API 13+
screenW = screenSize.x;
screenH = screenSize.y;
} catch (NoSuchMethodError e) { // The backup methods will only be used if the device is running pre-13, so it's fine that they were deprecated in API 13, thus the suppress warnings annotation at the start of the method.
screenW = display.getWidth();
screenH = display.getHeight();
}
}
// Check if view is outside right and bottom
if (coordinates[0] > screenW) return false;
if (coordinates[1] > screenH) return false;
// Else, view is (at least partially) in the screen bounds
return true;
}
To use it, just pass in any view or subclass of view (IE, just about anything that draws on screen in Android.) It'll return true if it's on screen or false if it's not... pretty intuitive, I think.
If you're not using the above method as a static, then you can probably get a context some other way, but in order to get the Application context from a static method, you need to do these two things:
1 - Add the following attribute to your application tag in your manifest:
android:name="com.package.MyApplication"
2 - Add in a class that extends Application, like so:
public class MyApplication extends Application {
// MyApplication exists solely to provide a context accessible from static methods.
private static Context context;
#Override public void onCreate() {
super.onCreate();
MyApplication.context = getApplicationContext();
}
public static Context getSharedContext() {
return MyApplication.context;
}
}
In addition to the view.getVisibility() there is view.isShown().
isShown checks the view tree to determine if all ancestors are also visible.
Although, this doesn't handle obstructed views, only views that are hidden or gone in either themselves or one of its parents.
In dealing with a similar issue, where I needed to know if the view has some other window on top of it, I used this in my custom View:
#Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus) {
} else {
}
}
This can be checked using getGlobalVisibleRect method. If rectangle returned by this method has exactly the same size as View has, then current View is completely visible on the Screen.
/**
* Returns whether this View is completely visible on the screen
*
* #param view view to check
* #return True if this view is completely visible on the screen, or false otherwise.
*/
public static boolean onScreen(#NonNull View view) {
Rect visibleRect = new Rect();
view.getGlobalVisibleRect(visibleRect);
return visibleRect.height() == view.getHeight() && visibleRect.width() == view.getWidth();
}
If you need to calculate visibility percentage you can do it using square calculation:
float visiblePercentage = (visibleRect.height() * visibleRect.width()) / (float)(view.getHeight() * view.getWidth())
This solution takes into account view obstructed by statusbar and toolbar, also as view outside the window (e.g. scrolled out of screen)
/**
* Test, if given {#code view} is FULLY visible in window. Takes into accout window decorations
* (statusbar and toolbar)
*
* #param view
* #return true, only if the WHOLE view is visible in window
*/
public static boolean isViewFullyVisible(View view) {
if (view == null || !view.isShown())
return false;
//windowRect - will hold available area where content remain visible to users
//Takes into account screen decorations (e.g. statusbar)
Rect windowRect = new Rect();
view.getWindowVisibleDisplayFrame(windowRect);
//if there is toolBar, get his height
int actionBarHeight = 0;
Context context = view.getContext();
if (context instanceof AppCompatActivity && ((AppCompatActivity) context).getSupportActionBar() != null)
actionBarHeight = ((AppCompatActivity) context).getSupportActionBar().getHeight();
else if (context instanceof Activity && ((Activity) context).getActionBar() != null)
actionBarHeight = ((Activity) context).getActionBar().getHeight();
//windowAvailableRect - takes into account toolbar height and statusbar height
Rect windowAvailableRect = new Rect(windowRect.left, windowRect.top + actionBarHeight, windowRect.right, windowRect.bottom);
//viewRect - holds position of the view in window
//(methods as getGlobalVisibleRect, getHitRect, getDrawingRect can return different result,
// when partialy visible)
Rect viewRect;
final int[] viewsLocationInWindow = new int[2];
view.getLocationInWindow(viewsLocationInWindow);
int viewLeft = viewsLocationInWindow[0];
int viewTop = viewsLocationInWindow[1];
int viewRight = viewLeft + view.getWidth();
int viewBottom = viewTop + view.getHeight();
viewRect = new Rect(viewLeft, viewTop, viewRight, viewBottom);
//return true, only if the WHOLE view is visible in window
return windowAvailableRect.contains(viewRect);
}
you can add to your CustomView's constractor a an onScrollChangedListener from ViewTreeObserver
so if your View is scrolled of screen you can call view.getLocalVisibleRect() and determine if your view is partly offscreen ...
you can take a look to the code of my library : PercentVisibleLayout
Hope it helps!
in your custom view, set the listeners:
getViewTreeObserver().addOnScrollChangedListener(this);
getViewTreeObserver().addOnGlobalLayoutListener(this);
I am using this code to animate a view once when it is visible to user.
2 cases should be considered.
Your view is not in the screen. But it will be visible if user scrolled it
public void onScrollChanged() {
final int i[] = new int[2];
this.getLocationOnScreen(i);
if (i[1] <= mScreenHeight - 50) {
this.post(new Runnable() {
#Override
public void run() {
Log.d("ITEM", "animate");
//animate once
showValues();
}
});
getViewTreeObserver().removeOnScrollChangedListener(this);
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
Your view is initially in screen.(Not in somewhere else invisible to user in scrollview, it is in initially on screen and visible to user)
public void onGlobalLayout() {
final int i[] = new int[2];
this.getLocationOnScreen(i);
if (i[1] <= mScreenHeight) {
this.post(new Runnable() {
#Override
public void run() {
Log.d("ITEM", "animate");
//animate once
showValues();
}
});
getViewTreeObserver().removeOnGlobalLayoutListener(this);
getViewTreeObserver().removeOnScrollChangedListener(this);
}
}

Categories

Resources