I am trying to get a screen capture in the Xamarin.Android platform.
public static Android.Content.Context Context { get; private set; }
public override View OnCreateView(View parent, string name, Context context, IAttributeSet attrs)
{
MainActivity.Context = context;
return base.OnCreateView(parent, name, context, attrs);
}
I am trying to find out the why the following rootView.Width and Height returns 0 all the time.
var rootView = ((Activity)MainActivity.Context).Window.DecorView.RootView;
Console.WriteLine ("{0}x{1}", rootView.Width,rootView.Height);
My ultimate goal is to capture the screenshot of the view as an image and generate pdf.
I don't know Xamarin, however it seems to be the same as native Android for this solution.
When onCreateView() is called the views have not yet been measured. To get a view dimensions you should attach a specific listener: onLayoutChangeListener.
Here is an Android native code example:
rootView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop,
int oldRight, int oldBottom) {
int width = right - left;
int height = bottom - top;
v.removeOnLayoutChangeListener(this); // Remove the listener
}
});
You could find here the listener to use for Xamarin
Hope its help ! :)
In the onCreateView the width & height of objects are not yet defined. They are defined in a later stage of the activity's lifecycle.
You have to use the treeviewobserver for this.
Example with your rootview:
rootView.ViewTreeObserver.GlobalLayout += (object sender, EventArgs e) => {
Console.WriteLine ("{0}x{1}", rootView.Width,rootView.Height);
};
In that method the width & height will be known.
Furthermore, you want to take a picture of the your rootview, the best way to do this is to use this method, this will automatically output a Bitmap of the view to variable b.
rootView.DrawingCacheEnabled = true;
Bitmap b = rootView.GetDrawingCache(true);
Hope this gets you on your way!
Related
I am trying to disable Android 10+ gesture navigation back swipes in my game (for a VERY valid reason). I need to disable swipe gestures for the whole screen.
Google's documentation is very vague about how to do this. https://developer.android.com/training/gestures/gesturenav#games
This is what I have tried. getWindow().setSystemGestureExclusionRects(exclusionRects) causes an instant crash on launch if I do it directly in my onCreate. The error I get is: java.lang.IllegalStateException: view not added.
My second attempt was setting system gesture exclusion rects inside an addOnLayoutChangeListener to the (only) view contained in my root layout so that the gesture exclusion stuff only runs once the view is good to go. For the sake of this example assume initializeForView is a function that returns a valid view. Everything works other than gestures not being blocked.
This code that I tried only blocks back swipe gestures from the lower half of the screen for some reason :(
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
game = new Lockjaw(this);
AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
RelativeLayout layout = new RelativeLayout(this);
layout.setFitsSystemWindows(true);
View view = initializeForView(game, config);
layout.addView(view);
setContentView(layout);
view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
List<Rect> exclusionRects = new ArrayList();
Rect visibleRect = new Rect();
v.getWindowVisibleDisplayFrame(visibleRect);
exclusionRects.add(visibleRect);
getWindow().setSystemGestureExclusionRects(exclusionRects);
}
}
});
}
Define this code in your Utils class.
static List<Rect> exclusionRects = new ArrayList<>();
public static void updateGestureExclusion(AppCompatActivity activity) {
if (Build.VERSION.SDK_INT < 29) return;
exclusionRects.clear();
Rect rect = new Rect(0, 0, SystemUtil.dpToPx(activity, 16), getScreenHeight(activity));
exclusionRects.add(rect);
activity.findViewById(android.R.id.content).setSystemGestureExclusionRects(exclusionRects);
}
public static int getScreenHeight(AppCompatActivity activity) {
DisplayMetrics displayMetrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
return height;
}
public static int dpToPx(Context context, int i) {
return (int) (((float) i) * context.getResources().getDisplayMetrics().density);
}
Check if your layout is set in that activity where you want to exclude the edge getures and then apply this code.
// 'content' is the root view of your layout xml.
ViewTreeObserver treeObserver = content.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
content.getViewTreeObserver().removeOnGlobalLayoutListener(this);
SystemUtil.updateGestureExclusion(MainHomeActivity.this);
}
});
We are adding the view width to 16dp to trigger the code when user swipe right from left edge & height to screen height to do it fully left side.
I have standard UI elements in rows within a ListView. When I scroll the list too far away from the UI elements and scroll back, they stop responding and the clicks fall through to the cell below.
Any ideas why this might happen?
If I don't scroll away from a UI element, it works. Also, if I scroll up and down a few times, elements will start working again.
It can't be my click handlers failing to fire because a picker will open up - and they don't need client code to open them.
I don't have an adapter. This is via Xamarin. I was hoping someone with some deeper knowledge of Android might have some insight.
I found this article and discovered that rendering a cell can be extremely simple. Then I found the ViewCellRenderer source code in Visual Studio. I copied it, hacked away a load of stuff and the bug has gone.
There was some suspicious stuff regarding handling long presses. Perhaps one day I will go back in and try and find exactly what was causing the problem.
It doesn't seem to have broken so far. I'll update it if it goes wrong.
This is what I'm left with...
public class ResponseViewCellRenderer : ViewCellRenderer
{
protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, ViewGroup parent, Context context)
{
if (convertView is ViewCellContainer container)
{
container.DisposeView();
}
return new ViewCellContainer(context, (ViewCell) item);
}
}
internal class ViewCellContainer : ViewGroup
{
readonly ViewCell viewCell;
readonly IVisualElementRenderer viewRenderer;
public ViewCellContainer(Context context, ViewCell viewCell) : base(context)
{
this.viewCell = viewCell;
viewRenderer = Platform.CreateRendererWithContext(viewCell.View, Context);
Platform.SetRenderer(viewCell.View, viewRenderer);
AddView(viewRenderer.View);
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
var width = Context.FromPixels(r - l);
var height = Context.FromPixels(b - t);
Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(viewRenderer.Element, new Rectangle(0.0, 0.0, width, height));
viewRenderer.UpdateLayout();
}
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
var size = MeasureSpec.GetSize(widthMeasureSpec);
var sizeRequest = viewRenderer.Element.Measure(Context.FromPixels(size), double.PositiveInfinity, MeasureFlags.IncludeMargins);
int measuredHeight = (int)Context.ToPixels((viewCell.Height > 0.0) ? viewCell.Height : sizeRequest.Request.Height);
SetMeasuredDimension(size, measuredHeight);
}
public void DisposeView()
{
Platform.SetRenderer(viewCell.View, null);
RemoveView(viewRenderer.View);
viewRenderer.View.Dispose();
}
}
I'm trying to do something once the setVisibility function finish it's work, is there anyway that I can accomplish that? because OnSystemUiVisibilityChangeListener isn't what I need , and I can't find any method that allows me to!
As see here:
ViewTreeObserver.html#addOnGlobalLayoutListener:
Register a callback to be invoked when the global layout state or the
visibility of views within the view tree changes
So you can do it by adding addOnGlobalLayoutListener to ImageView like:
imageView.setTag(imageView.getVisibility());
....
#Override
public void onGlobalLayout() {
int visibility = imageView.getVisibility();
if(Integer.parseInt(imageView.getTag()) != visibility)
{
// visibility changes...
}
}
....
You can try this one it will get triggered every time your layout changes
imageView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
Log.e("PRACTICE","VISIBILITY IS CHANGED");
}
});
I want to get the number of lines of a text view
textView.setText("Test line 1 Test line 2 Test line 3 Test line 4 Test line 5.............")
textView.getLineCount(); always returns zero
Then I have also tried:
ViewTreeObserver vto = this.textView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
ViewTreeObserver obs = textView.getViewTreeObserver();
obs.removeGlobalOnLayoutListener(this);
System.out.println(": " + textView.getLineCount());
}
});
It returns the exact output.
But this works only for a static layout.
When I am inflating the layout dynamically this doesn't work anymore.
How could I find the number of line in a TextView?
I was able to get getLineCount() to not return 0 using a post, like this:
textview.setText(“Some text”);
textview.post(new Runnable() {
#Override
public void run() {
int lineCount = textview.getLineCount();
// Use lineCount here
}
});
As mentioned in this post,
getLineCount() will give you the correct number of lines only after
a layout pass.
It means that you need to render the TextView first before invoking the getLineCount() method.
ViewTreeObserver is not so reliable especially when using dynamic layouts such as ListView.
Let's assume:
1. You will do some work depending on the lines of TextView.
2. The work is not very urgent and can be done later.
Here is my solution:
public class LayoutedTextView extends TextView {
public LayoutedTextView(Context context) {
super(context);
}
public LayoutedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutedTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public interface OnLayoutListener {
void onLayouted(TextView view);
}
private OnLayoutListener mOnLayoutListener;
public void setOnLayoutListener(OnLayoutListener listener) {
mOnLayoutListener = listener;
}
#Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mOnLayoutListener != null) {
mOnLayoutListener.onLayouted(this);
}
}
}
Usage:
LayoutedTextView tv = new LayoutedTextView(context);
tv.setOnLayoutListener(new OnLayoutListener() {
#Override
public void onLayouted(TextView view) {
int lineCount = view.getLineCount();
// do your work
}
});
textView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
// Remove listener because we don't want this called before _every_ frame
textView.getViewTreeObserver().removeOnPreDrawListener(this)
// Drawing happens after layout so we can assume getLineCount() returns the correct value
if(textView.getLineCount() > 2) {
// Do whatever you want in case text view has more than 2 lines
}
return true; // true because we don't want to skip this frame
}
});
I think the crux of this question is that people want to be able to find out the size of a TextView in advance so that they can dynamically resize it to nicely fit the text. A typical use might be to create talk bubbles (at least that was what I was working on).
I tried several solutions, including use of getTextBounds() and measureText() as discussed here. Unfortunately, both methods are slightly inexact and have no way to account for line breaks and unused linespace. So, I gave up on that approach.
That leaves getLineCount(), whose problem is that you have to "render" the text before getLineCount() will give you the number of lines, which makes it a chicken-and-egg situation. I read various solutions involving listeners and layouts, but just couldn't believe that there wasn't something simpler.
After fiddling for two days, I finally found what I was looking for (at least it works for me). It all comes down to what it means to "render" the text. It doesn't mean that the text has to appear onscreen, only that it has to be prepared for display internally. This happens whenever a call is made directly to invalidate() or indirectly as when you do a setText() on your TextView, which calls invalidate() for you since the view has changed appearance.
Anyway, here's the key code (assume you already know the talk bubble's lineWidth and lineHeight of a single line based on the font):
TextView talkBubble;
// No peeking while we set the bubble up.
talkBubble.setVisibility( View.INVISIBLE );
// I use FrameLayouts so my talk bubbles can overlap
// lineHeight is just a filler at this point
talkBubble.setLayoutParams( new FrameLayout.LayoutParams( lineWidth, lineHeight ) );
// setText() calls invalidate(), which makes getLineCount() do the right thing.
talkBubble.setText( "This is the string we want to dynamically deal with." );
int lineCount = getLineCount();
// Now we set the real size of the talkBubble.
talkBubble.setLayoutParams( new FrameLayout.LayoutParams( lineWidth, lineCount * lineHeight ) );
talkBubble.setVisibility( View.VISIBLE );
Anyway, that's it. The next redraw will give a bubble tailor-made for your text.
Note: In the actual program, I use a separate bubble for determining lines of text so that I can resize my real bubble dynamically both in terms of length and width. This allows me to shrink my bubbles left-to-right for short statements, etc.
Enjoy!
You could also use PrecomputedTextCompat for getting the number of lines.
Regular method:
fun getTextLineCount(textView: TextView, text: String, lineCount: (Int) -> (Unit)) {
val params: PrecomputedTextCompat.Params = TextViewCompat.getTextMetricsParams(textView)
val ref: WeakReference<TextView>? = WeakReference(textView)
GlobalScope.launch(Dispatchers.Default) {
val text = PrecomputedTextCompat.create(text, params)
GlobalScope.launch(Dispatchers.Main) {
ref?.get()?.let { textView ->
TextViewCompat.setPrecomputedText(textView, text)
lineCount.invoke(textView.lineCount)
}
}
}
}
Call this method:
getTextLineCount(textView, "Test line 1 Test line 2 Test line 3 Test line 4 Test line 5.............") { lineCount ->
//count of lines is stored in lineCount variable
}
Or maybe you can create extension method for it like this:
fun TextView.getTextLineCount(text: String, lineCount: (Int) -> (Unit)) {
val params: PrecomputedTextCompat.Params = TextViewCompat.getTextMetricsParams(this)
val ref: WeakReference<TextView>? = WeakReference(this)
GlobalScope.launch(Dispatchers.Default) {
val text = PrecomputedTextCompat.create(text, params)
GlobalScope.launch(Dispatchers.Main) {
ref?.get()?.let { textView ->
TextViewCompat.setPrecomputedText(textView, text)
lineCount.invoke(textView.lineCount)
}
}
}
}
and then you call it like this:
textView.getTextLineCount("Test line 1 Test line 2 Test line 3 Test line 4 Test line 5.............") { lineCount ->
//count of lines is stored in lineCount variable
}
Based on #secnelis idea, there is even a more clean way if you target API 11 or higher.
Instead of extending a TextView you can use already built-in functionality if View.OnLayoutChangeListener
In ListAdapter.getView(), for instance
if (h.mTitle.getLineCount() == 0 && h.mTitle.getText().length() != 0) {
h.mTitle.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(final View v, final int left, final int top,
final int right, final int bottom, final int oldLeft,
final int oldTop, final int oldRight, final int oldBottom) {
h.mTitle.removeOnLayoutChangeListener(this);
final int count = h.mTitle.getLineCount();
// do stuff
}
});
} else {
final int count = h.mTitle.getLineCount();
// do stuff
}
You can also calculate the amount of lines through this function:
private fun countLines(textView: TextView): Int {
return Math.ceil(textView.paint.measureText(textView.text.toString()) /
textView.measuredWidth.toDouble()).toInt()
}
Keep in mind that It may not work very well on a RecyclerView though.
textview.getText().toString().split(System.getProperty("line.separator")).length
It works fine for me to get number of lines of TextView.
Are you doing this onCreate? The Views aren't laid out yet, so getLineCount() is 0 for a while. If you do this later in the Window LifeCycle, you'll get your line count. You'll have a hard time doing it onCreate, but onWindowFocusChanged with hasFocus=true usually has the Views measured by now.
The textView.post() suggestion is also a good one
Dianne Hackborn mentioned in a couple threads that you can detect when a layout as been resized, for example, when the soft keyboard opens or closes. Such a thread is this one... http://groups.google.com/group/android-developers/browse_thread/thread/d318901586313204/2b2c2c7d4bb04e1b
However, I didn't understand her answer: "By your view hierarchy being resized with all of the corresponding layout traversal and callbacks."
Does anyone have a further description or some examples of how to detect this? Which callbacks can I link into in order to detect this?
Thanks
One way is View.addOnLayoutChangeListener. There's no need to subclass the view in this case. But you do need API level 11. And the correct calculation of size from bounds (undocumented in the API) can sometimes be a pitfall. Here's a correct example:
view.addOnLayoutChangeListener( new View.OnLayoutChangeListener()
{
public void onLayoutChange( View v,
int left, int top, int right, int bottom,
int leftWas, int topWas, int rightWas, int bottomWas )
{
int widthWas = rightWas - leftWas; // Right exclusive, left inclusive
if( v.getWidth() != widthWas )
{
// Width has changed
}
int heightWas = bottomWas - topWas; // Bottom exclusive, top inclusive
if( v.getHeight() != heightWas )
{
// Height has changed
}
}
});
Another way (as dacwe answers) is to subclass your view and override onSizeChanged.
Override onSizeChanged in your View!
With Kotlin extensions:
inline fun View?.onSizeChange(crossinline runnable: () -> Unit) = this?.apply {
addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
val rect = Rect(left, top, right, bottom)
val oldRect = Rect(oldLeft, oldTop, oldRight, oldBottom)
if (rect.width() != oldRect.width() || rect.height() != oldRect.height()) {
runnable();
}
}
}
Use thus:
myView.onSizeChange {
// Do your thing...
}
My solution is to add an invisible tiny dumb view at the end of of the layout / fragment (or add it as a background), thus any change on size of the layout will trigger the layout change event for that view which could be catched up by OnLayoutChangeListener:
Example of adding the dumb view to the end of the layout:
<View
android:id="#+id/theDumbViewId"
android:layout_width="1dp"
android:layout_height="1dp"
/>
Listen the event:
View dumbView = mainView.findViewById(R.id.theDumbViewId);
dumbView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
// Your code about size changed
}
});
Thank to https://stackoverflow.com/users/2402790/michael-allan this is the good and simple way if you don't want to override all your views.
As Api has evolved I would suggest this copy paste instead:
String TAG="toto";
///and last listen for size changed
YourView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v,
int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
boolean widthChanged = (right-left) != (oldRight-oldLeft);
if( widthChanged )
{
// width has changed
Log.e(TAG,"Width has changed new width is "+(right-left)+"px");
}
boolean heightChanged = (bottom-top) != (oldBottom-oldTop);
if( heightChanged)
{
// height has changed
Log.e(TAG,"height has changed new height is "+(bottom-top)+"px");
}
}
});
View.doOnLayout(crossinline action: (view: View) -> Unit): Unit
See docs.
Performs the given action when this view is laid out. If the view has been laid out and it has not requested a layout, the action will be performed straight away, otherwise the action will be performed after the view is next laid out.
The action will only be invoked once on the next layout and then removed.
Kotlin version base on #Michael Allan answer to detect layout size change
binding.root.addOnLayoutChangeListener { view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
if(view.height != oldBottom - oldTop) {
// height changed
}
if(view.width != oldRight - oldLeft) {
// width changed
}
}
Other answers are correct, but they didn't remove OnLayoutChangeListener. So, every time this listener was called, it added a new listener and then called it many times.
private fun View.onSizeChange(callback: () -> Unit) {
addOnLayoutChangeListener(object : OnLayoutChangeListener {
override fun onLayoutChange(
view: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int,
) {
view?.removeOnLayoutChangeListener(this)
if (right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop) {
callback()
}
}
})
}
If your like me and you came to this page and
addOnLayoutChangeListener gave you functionality you needed but caused an infinite list of "requestLayout() improperly called .... running second layout pass" messages to fill your logcat and ...
Adding removingOnLayoutChangeListener(this) broke the functionality you had in 1) despite fixing the repeated logcat message then this might work.
Its a mix of what Slion and Tony suggested.
Create a ResetDetectorCallback interface with abstract method
resizeAction.
Create a simple custom view ResetDetector with an instance of the ResetDetectorCallback, a setResetDetectorCallback mehtod, and override its onSizeChange() method to invok the resetDectectorCallback.resizeAction().
In the layout place an
instance of the ResetDetectorView with an id, width="match_parent", height="match_parent", and in my case I also constrained it.
Then in the code (mine was a Fragment since I'm adopting the Single Activity methodology) let that class implement the ResetDetectorCallback, set the resetDetectorView from that class's layout to use "this", and implement the resizeAction in that class.
And in my case its finally working the way I want now. On a Desktop emulator I can resize that Window as much as I want and it appears tobe accurately adjusting the layout accordingly to the way I want and the logcat is not being overloaded with "requestLayout() improperly called .... running second layout pass" messages.
There is probably a better way, perhaps with ViewModels and mutableStateObservers, but I'm not familiar enough with those yet. I hope this helps someone somewhere. And thanks to all the contributors above even if your technique didn't work for me.