I am writing Instrumentation tests for my android app. One thing I want to do is to find whether all the UI components are on the screen or not. For that I have taken the screen shot of the complete screen and then I am looking for a particular widget in that image.
This code need to be running on the device only, not on desktop.
E.g. the full screen shot (image-1) have various android components like textview, button, listview and a image. Now I have a subset of this image (image-2), suppose the image of the button.
How can I find that whether image-2 is part of image-1?
Assuming that this code is happening from within the application, it doesn't seem like image comparison is the easiest way to determine whether a view is visible.
If you are writing an external instrumentation application of some sort, and this answer doesn't apply, please let me know.
Here's what I would do to test for the presence of UI elements from within the app:
From the Android API docs on the View object: you can find a view by its ID that was set up in the XML file:
<Button
android:id="#+id/my_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/my_button_text"/>
In the App:
Button myButton = (Button) findViewById(R.id.my_button);
Then, check the getVisibility() and getGlobalVisibleRect (Rect r, Point globalOffset), both documented on the View doc page.
Pseudocode:
int[] viewIds = {<known ids from xml>};
foreach(int viewId in viewIds) {
View v = findViewById(viewId);
if (v!=null) {
bool isVisible = (v.getVisibility()==VISIBLE) && getGlobalVisibleRect(new Rect(), new Point());
// do something with the visible/invisible info
}
}
Related
Some other users and I are developing an Android application for the Stack Exchange chat network. We're adding a tutorial for each activity to explain the UI to the user. However, we've run into a bit of a road block.
The tutorial library wants a pointer to a view (be it a TextView, ImageView, whatever) in order to get the coordinates of the view in the display so it knows where to draw the drop shadows and stuff.
We have one activity which uses the standard "Tabbed Activity" from Android Studio, so we aren't using any custom toolbars.
The action bar looks like this:
And we want to grab a pointer to the TextView on each tab that holds the title of the tab.
So for example, we want to be able to access this Textview:
We haven't been real successful in finding anything on the internet about how to do this. It appears to be relatively easy if you're using a custom toolbar, but we aren't.
Digging in the AOSP source code, we found a potential way to do it, but the fields that we needed access to were either private or otherwise unaccessible from the main activity code.
So the question is, how can we grab a pointer to that TextView? Is it even possible?
Well, it isn't pretty but we found a way to do it. Using the layout inspector in Android Device Monitor to look at the view hierarchy, we were able to grab a pointer to it in the following way.
Keep in mind:
You may need to adjust for your activity's layout
If you're using a custom toolbar there's an easier way to do this
That being said, here's what worked for this specific use case:
ViewGroup viewGroup = (ViewGroup) getWindow().getDecorView();
LinearLayout testb = (LinearLayout) viewGroup.getChildAt(0);
FrameLayout testc = (FrameLayout) testb.getChildAt(1);
ActionBarOverlayLayout testd = (ActionBarOverlayLayout) testc.getChildAt(0);
ActionBarContainer teste = (ActionBarContainer) testd.getChildAt(1);
LinearLayoutCompat testg;
if (getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT)
{
ScrollingTabContainerView testf = (ScrollingTabContainerView) teste.getChildAt(2);
testg = (LinearLayoutCompat) testf.getChildAt(0);
}
else //Landscape
{
Toolbar teste2 = (Toolbar) teste.getChildAt(0);
ScrollingTabContainerView testf = (ScrollingTabContainerView) teste2.getChildAt(0);
testg = (LinearLayoutCompat) testf.getChildAt(0);
}
testg.setId(android.R.id.tabcontent);
//String IdAsString = testg.getResources().getResourceName(testg.getId());
//Log.e("TestG", IdAsString);
TutorialStuff.chatsExplorationTutorial(this, testg);
And here's the end result:
I took the source I found here:
https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Platform.Android/Renderers/MasterDetailRenderer.cs
And created a custom renderer in a Xamarin Forms Android project. I got the project to build and the menu to open / close as expected as well as display the starting detail page.
The detail page is a "NavigaionPage" in Xamarin.Forms which is actually just a ViewGroup with a bunch of code to make it work with Push / Pop functions.
When I push a new Page, one that has a custom renderer and native RelativeLayout as a subview, the Page appears blank white until I rotate orientation.
After some research in the code, I realized both OnMeasure and OnLayout was not being called in the Page which was being pushed to the NavigationPage / detail page (with the pushed page's size stuck at 0x0 until orientation change).
I created another custom renderer but this time for the NavigationPage which looks like this:
public class MasterDetailContentNavigationPageRenderer : NavigationPageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e)
{
base.OnElementChanged(e);
if(e.OldElement != null)
{
e.OldElement.Pushed -= handleOnPushed;
}
if(e.NewElement != null)
{
e.NewElement.Pushed += handleOnPushed;
}
}
private void handleOnPushed(object sender, EventArgs e)
{
var w = MeasureSpec.MakeMeasureSpec(MeasuredWidth, MeasureSpecMode.Exactly);
var h = MeasureSpec.MakeMeasureSpec(MeasuredHeight, MeasureSpecMode.Exactly);
Measure(w, h);
Layout(Left, Top, Right, Bottom);
}
}
This custom renderer for the detail page / NavigaionPage worked for 2 out of 3 of my pushed Pages.
The main / most important page did not fully add all of the UI elements.
The Page that was pushed and missing UI has SOME of the UI. The following is a list of how the page is displayed:
--- SongViewerLayout : RelativeLayout
------ RelativeLayout
--------- ViewerTopBar (draws all buttons except one)
--------- DocumentView (blank white spot between top bar and pagination)
--------- ViewerPagination (draws background color but not page buttons)
If I change orientations the missing button appears in the top bar, the page numbers show up and the document view sort of draws but it's off.
If I go back to using Xamarin.Forms provided MasterDetailPage the UI just loads as expected.
I have studied the code in the Xamarin.Forms repo but none of it really applies to the inner workings of the elements that are added. In other words, the detail page (which in my case is a NavigationPage) gets added on SetElement(...) and some handlers are assigned ... but nothing I could tell that looks for Pages being pushed and then responding. I would assume / expect the NavigationPage just to work as expected now that it's a subview of the MasterDetailPage / DrawerLayout.
Note: the views that seems to have the most issues are added programmatically and given "LayoutParamaters". The views found in the AXML seem to just show up.
Is there something I am missing to "initialize" a layout in Android? I have made native apps in Android and not had to do anything special.
Why would the UI above the NavigationPage cause the NavigationPage not to do standard initialization?
What am I missing???
Edit:
I created a working example of this issue on my GitHub account which you can clone here:
https://github.com/XamarinMonkey/CustomMasterDetail
Build and run CustomMasterDetail.Droid (Notice the UI is a MasterDetailPage)
Tap any item
The bottom blue bar has no page numbers.
Rotate orientation the page numbers display.
Stop the build
Comment out the entire class "MainMasterDetailPageRenderer.cs"
Rebuild / run the app
Tap any item
Page numbers are shown right away as expected just because it is now using the default MasterDetailPage!
Give suggestions please!
Edit #2:
Thanks to some feedback, it does not seem to be an issue in 6.0 devices. My testing device is a "Samsung Galaxy Tab Pro" with v5.1.1. And unfortunately I need this app to go back to 4!
Edit #3:
The issue has to be adding a view programmatically in Android 5.1.1 to a native RelativeLayout (which is pushed to a NavigationPage detail page ) after it is inflated in Xamarin.Forms. Calling Measure / Layout manually seems to solve the issue. But I need a more automated solution.
I came up with a solution that works. It might be expensive but it seems to layout fast on devices I have tested.
Basicly, I read up on Android and found a way to listen for layout changes using the ViewTreeObserver.GlobalLayout event combined with calling Measure / Layout on the detail and master ViewGroups.
public void MeasureAndLayoutNative()
{
if(_childView != null)
{
IVisualElementRenderer renderer = Platform.GetRenderer(_childView);
if(renderer.ViewGroup != null)
{
var nativeView = renderer.ViewGroup;
var w = MeasureSpec.MakeMeasureSpec(nativeView.MeasuredWidth, MeasureSpecMode.Exactly);
var h = MeasureSpec.MakeMeasureSpec(nativeView.MeasuredHeight, MeasureSpecMode.Exactly);
nativeView.Measure(w, h);
nativeView.Layout(nativeView.Left, nativeView.Top, nativeView.Right, nativeView.Bottom);
}
}
}
I updated my code example above with the working changes!
I have an activity A. I am creating a kind of tutorial for user for this activity, to teach him how he can use the app on that screen.
For that, my requirement is :
I want to blur all the views of the activity except one view. I want to prompt user to click on that view through a hand image pointing at that view.
Nothing should happen if the user clicks on the blurred/greyed out area, but if he taps on that particular active view, it should react to that touch.
I was thinking of using a full screen fragment for this. The Fragment will take the following input from the activity :
for what coordinates, is should not blur the screen and pass the touch event to the activity
the coordinates on which it should show that pointing hand image.
After from these coordinates, the fragment background would be blur.
I wanted to confirm if that's possible, to make the fragment partially active, i.e. delegate it's touch events to the activity for a particular view of the activity.
Also, please let me know if there is any other better approach of achieving the same thing.
Edit1 :
Thinking of using a fragment here, because I'd want this type of behaviour on different screen in future. In that case, I'd make that fragment generic which takes some inputs (as described above) and use it on different screens.
There's a very good library called SCV which does what you're trying to achieve, you're able to customize the styles for it too. I've used this for first time the app is opened to show the user a tutorial.
According to their Github
The ShowcaseView (SCV) library is designed to highlight and showcase specific parts of apps to the user with a distinctive and attractive overlay. This library is great for pointing out points of interest for users, gestures, or obscure but useful items.
Further Reading:
Android Arsenal - Showcase Views Tutorial
ShowCaseView on Android - Indipendev
I found it much easier to include an 'extra' layout around the UI of my activity, and then to add a highest-z grey mostly-transparent filter to it and put the instructions on that.
Each "step" of the instructions was a different layout that was dynamically loaded into that layout container as they clicked. (Just another approach)
The 'container' layout is a: FrameLayout
then in my Activity I have: (ignore bad naming)
private void addOverlayLayout() {
frameLayout = (FrameLayout) findViewById(R.id.framelayoutInner);
frameLayout3 = (FrameLayout) findViewById(R.id.framelayout3);
frameLayout3.setBackgroundColor(Color.DKGRAY);
frameLayout3.setAlpha(0.3f);
// Dynamically create a relativelayout which will be appended to framelayout
relativeLayout = new RelativeLayout(getApplicationContext());
relativeLayout.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams
.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
instructionOverlays.add(createSimpleClickInstruction(R.layout.instruction_reader_1));
instructionOverlays.add(createSimpleClickInstruction(R.layout.instruction_reader_2));
if (FullscreenReaderActivity.isFirstRun) {
displayNextGuide();
}
}
public void displayNextGuide() {
// clean relative layout if it has views
relativeLayout.removeAllViews();
// clean frame layout if it has child (safe if empty)
frameLayout.removeView(relativeLayout);
if (!isFirstRun) {
return;
}
if (instructionOverlays.size() > 0) {
runOnUiThread(instructionOverlays.get(0));
instructionOverlays.remove(0);
} else {
frameLayout3.setBackgroundColor(Color.TRANSPARENT);
frameLayout3.setAlpha(1.0f);
}
}
public Runnable createSimpleClickInstruction(final int resource) {
return new Runnable() {
#Override
public void run() {
getLayoutInflater().inflate(
resource,
relativeLayout,
true
);
relativeLayout.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
displayNextGuide();
}
});
frameLayout.addView(relativeLayout);
}
};
}
EDIT: tl;dr: WebView appears as white box, even though I appear to be setting it up correctly, and indeed it does work the first two times, but fails subsequently)
EDIT: Video showing the problem in action...
I have the following bit of code which inflates a view (Which contains a WebView) from the xml which defines it:
private void createCard(ViewGroup cvFrame, Card card) {
//... setup vairables...
cvFrame.clearDisappearingChildren();
cvFrame.clearAnimation();
try {
View cv = LayoutInflater.from(getBaseContext()).inflate(R.layout.card_back_view,
cvFrame, true);
cv.setBackgroundDrawable(Drawable.createFromStream(mngr.open(deckName + "_Card_back.png"), deckName));
TextView suit = (TextView)cv.findViewWithTag("card_back_suit");
//...setup text view for suit, this code works fine every time...
WebView title = (WebView)cv.findViewWithTag("card_back_title");
//This WebView doesn't appear to be the one which actually appears on screen (I can change settings till I'm blue in the face, with no effect)
if (title != null) {
title.setBackgroundColor(0x00000000);
title.loadData(titleText, "text/html", "UTF-8");
} else {
Log.e("CardView", "Error can't find title WebView");
}
} catch (IOException e) {
Log.e("CardView", "Error making cards: ", e);
}
}
When this method is called as part of the onCreate method in my Activity, the WebView contains the correct code, and is suitably transparent.
I have a gesture listener which replaces the contents of the ViewGroup with different content (It animates the top card off to the left, replaces the contents of the top card with card 2, puts the top card back, then replaces card 2 with card 3)
//Gesture listener event
ViewGroup cvFrame = (ViewGroup)findViewById(R.id.firstCard);
cardLoc++
cvFrame.startAnimation(slideLeft);
(onAnimationEnd code)
public void onAnimationEnd(Animation animation) {
if (animation == slideLeft) {
ViewGroup cvFrameOldFront = (ViewGroup)findViewById(R.id.firstCard);
ViewGroup cvFrameNewFront = (ViewGroup)findViewById(R.id.secondCard);
createCard(cvFrameOldFront, cards.get((cardLoc)%cards.size()));
createCard(cvFrameNewFront, cards.get((cardLoc+1)%cards.size()));
TranslateAnimation slideBack = new TranslateAnimation(0,0,0,0);
slideBack.setDuration(1);
slideBack.setFillAfter(true);
cvFrameOldFront.startAnimation(slideBack);
}
}
When the animation has happened and I replace the contents of the cards, the TextView suit is replaced fine and the code definitely passes through the code to replace the WebView contents, but for some reason I end up with a white rectangle the size and shape of the WebView, no content, no transparency.
If I change the WebView to a TextView, it's contents is replaced fine, so it's an issue that occurs only with the WebView control :S
Can anyone tell me why / suggest a fix?
It turns out the WebView doesn't get cleared down when using the LayoutInflater to replace the contents of a ViewGroup. The other controls all seem to get removed (or at least the findViewWithTag() returns the right reference for every other control). I've just added in the line cvFrame.removeAllViews() immediately before the LayoutInflater does it's stuff and that fixed the issue.
If anyone has any better explanation for this I'll throw the points their way otherwise they will just go into the ether...
By calling findViewById, you are getting a reference on the previously loaded webview do you ?
so the loadData call that fails is the second one you make on a single webview instance.
you may want to check this :
Android WebView - 1st LoadData() works fine, subsequent calls do not update display
It appears that loadData() won't load data twice... you may want to try WebView.loadDataWithBaseUri()
Hope that helps.
I had a similar problem loading several WebViews content.
It was because of a misusing of the pauseTimers function
The situation was : the first webView weren't needed anymore, conscientiously I wanted to pause it before to release it. Calling onPause() and pauseTimers()
pauseTimers being common to any web views, it broke every use of webviews occuring after that, there were displaying only white rectangles.
Maybe its not your problem here, but it's worth checking your not calling WebView.pauseTimers() somewhere.
To confirm your answer, the source code for LayoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot) does in fact internally calls root.addView() which attaches the newly inflated view at the end of the root's children instead of replacing them.
So the mystery now is why did your call to findViewWithTag() is returning the expected objects for your other widgets (which would be the top, most recently created instances), but for your WebView it was returning something else.
Is it possible that there is another object in your layout XML which shares the same "card_back_title" tag?
I was also wondering why you didn't use the more common findViewById() instead, but I am not sure whether it would make a difference.
Trying to make an Android InputMethod that is transparent - i.e. the underlying content shows through to the keyboard that I am developing.
I've been able to make the View that I pass to the system transparent - I think - but there seems to be something underneath my view that is solid white - and obfuscating the underlying content.
It is definitely possible, these guys do it:
https://play.google.com/store/apps/details?id=com.aitype.android.tablet.p&feature=search_result#?t=W251bGwsMSwxLDEsImNvbS5haXR5cGUuYW5kcm9pZC50YWJsZXQucCJd
I figured it out! Not sure if this is how the guys in your play store link did it, but this is what worked for me. Also, I realize this post is over a year old, but I'm still answering it just in case someone else out there discovers this when trying to create a transparent keyboard.
The "something" under your view is actually nothing - it's empty space. Your keyboard pushed the entire view up and out of the way to make room for its height, leaving empty white space behind. Your transparent keyboard let this white space show through.
Here's the solution: instead of returning your view in onCreateInputView, return it in onCreateCandidatesView. That's the view that normally lives above the keyboard and lists the autocorrect suggestions. But you're going to use this to house your actual keyboard.
The reason you want to have your keyboard be a candidates view is because the input view most often pushes the underlying view up. Individual apps can decide how they want to behave when a keyboard is shown via android:windowSoftInputMode and the input view respects their preference, but the candidates view always uses adjustPan.
From the docs: "Note that because the candidate view tends to be shown and hidden a lot, it does not impact the application UI in the same way as the soft input view: it will never cause application windows to resize, only cause them to be panned if needed for the user to see the current focus." http://developer.android.com/reference/android/inputmethodservice/InputMethodService.html
So, return your transparent view from onCreateCandidatesView, return null from onCreateInputView and make sure to call setCandidatesViewShown(true) so your candidates view shows up (I call it in onWindowShown).
Normally InputMethodServices uses background color which is same with current binding application's background color. If you want to make this transparent, I think you should make it as popup-window structure, not an inputmethod window I think.
It may such easy to make the full screen keyboard layout extra area transparent via java reflection only if you're quite familiar with InputMethodService.
the extra area has an id name fullscreenArea, you can fetch the area's id, then findViewById() then set its background.
the keyboard look as this before I done my practice :
a giant blank cover the below page.
so after is :
you can see the below page which contained an EditText and others displayed.
here is my code :
public static void makeKeyboardTransparent(InputMethodService service) {
try {
View decorView = service.getWindow().getWindow().getDecorView();
final int viewId = fetchInternalRId("fullscreenArea");
View fullscreenArea = decorView.findViewById(viewId);
if (fullscreenArea != null) {
modifyView(fullscreenArea);
return;
}
} catch (Exception e) {
}
try {
Class<?> superClass = service.getClass().getSuperclass();
Field fullscreenAreaField = superClass.getDeclaredField("mFullscreenArea");
fullscreenAreaField.setAccessible(true);
View fullscreenArea = (View) fullscreenAreaField.get(service);
if (fullscreenArea != null) {
modifyView(fullscreenArea);
}
} catch (Exception e) {
}
}
private static void modifyView(View fullscreenArea) {
fullscreenArea.setBackgroundColor(Color.TRANSPARENT);
}
private static int fetchInternalRId(String name) throws Exception {
Class<?> rIdClass = Class.forName("com.android.internal.R$id");
return rIdClass.getDeclaredField(name).getInt(rIdClass);
}
I provided two approach to make the blank area transparent, both of them worked fine in my test, all you need is pass your InputMethodService into makeKeyboardTransparent() and see what it can do.