print a view hierarchy on a device - android

Debugging my app with strange results on Samsung phones, which I don't have physical access to. I'd like to ask a user to run an instrumented App to help debug. My App gets a view that has an unknown (to me ;-) hierarchy in it (ViewGroups etc). Is there a way to "walk" a View and print out to a string / ddms all the components in the View (etc ViewGroups in it etc)?
This would be akin to HierarchyViewer tool - if I had adb level access to the device.
Update: I guess I could use the method
void dumpViewHierarchyWithProperties(Context context, ViewGroup group, BufferedWriter out, int level)
from the ViewDebug.java Android OS sources ...
Anyone have a better idea?
Thanks!

Here's the utility function I just made for this purpose:
public static void printViewHierarchy(ViewGroup vg, String prefix) {
for (int i = 0; i < vg.getChildCount(); i++) {
View v = vg.getChildAt(i);
String desc = prefix + " | " + "[" + i + "/" + (vg.getChildCount()-1) + "] "+ v.getClass().getSimpleName() + " " + v.getId();
Log.v("x", desc);
if (v instanceof ViewGroup) {
printViewHierarchy((ViewGroup)v, desc);
}
}
}

I've created utility method which returns hierarchy in a pretty printed way with human readable view ids. Here is an example of the output:
[LinearLayout] no_id
[CardView] com.example:id/card_view
[RelativeLayout] no_id
[LinearLayout] com.example:id/image_container
[AppCompatImageView] com.example:id/incident_icon
[CustomTextView] com.example:id/road_number
[RelativeLayout] no_id
[CustomTextView] com.example:id/distance_to_user
[CustomTextView] com.example:id/obstruction_title
[CustomTextView] com.example:id/road_direction
[CustomTextView] com.example:id/obstruction_description
[AppCompatImageView] com.example:id/security_related
Here is the utility method:
public static String getViewHierarchy(#NonNull View v) {
StringBuilder desc = new StringBuilder();
getViewHierarchy(v, desc, 0);
return desc.toString();
}
private static void getViewHierarchy(View v, StringBuilder desc, int margin) {
desc.append(getViewMessage(v, margin));
if (v instanceof ViewGroup) {
margin++;
ViewGroup vg = (ViewGroup) v;
for (int i = 0; i < vg.getChildCount(); i++) {
getViewHierarchy(vg.getChildAt(i), desc, margin);
}
}
}
private static String getViewMessage(View v, int marginOffset) {
String repeated = new String(new char[marginOffset]).replace("\0", " ");
try {
String resourceId = v.getResources() != null ? (v.getId() > 0 ? v.getResources().getResourceName(v.getId()) : "no_id") : "no_resources";
return repeated + "[" + v.getClass().getSimpleName() + "] " + resourceId + "\n";
} catch (Resources.NotFoundException e) {
return repeated + "[" + v.getClass().getSimpleName() + "] name_not_found\n";
}
}
Tip: we use this method to add view hierarchies to some crash reports. In some cases it is really helpful.

almost the same with this answer but with Kotlin:
fun getViewTree(root: ViewGroup): String{
fun getViewDesc(v: View): String{
val res = v.getResources()
val id = v.getId()
return "[${v::class.simpleName}]: " + when(true){
res == null -> "no_resouces"
id > 0 -> try{
res.getResourceName(id)
} catch(e: android.content.res.Resources.NotFoundException){
"name_not_found"
}
else -> "no_id"
}
}
val output = StringBuilder(getViewDesc(root))
for(i in 0 until root.getChildCount()){
val v = root.getChildAt(i)
output.append("\n").append(
if(v is ViewGroup){
getViewTree(v).prependIndent(" ")
} else{
" " + getViewDesc(v)
}
)
}
return output.toString()
}

The format of other outputs dissatisfied my eyes and some codes where an overhead. So, I improved the output with:
/**
* Prints the view hierarchy.
*/
public static void printViewHierarchy(ViewGroup parent, String intent) {
for (int i = 0, max = parent.getChildCount(), numLenght = (max + "").length(); i < max; i++) {
View child = parent.getChildAt(i);
String childString = child.getClass().getSimpleName() + " " + child.getId();
String format = "|— %0" + numLenght + "d/%0" + numLenght + "d %s";
Log.d("debug", intent + String.format(format, i + 1, max, childString));
if (child instanceof ViewGroup)
printViewHierarchy((ViewGroup) child, intent + " ");
}
}
to:
|— 1/4 ScrollView 2131296482
|— 2/4 MaterialTextView 2131296445
|— 3/4 FloatingActionButton 2131296374
|— 4/4 MaterialTextView 2131296363
|— 1/4 ScrollView 2131296482
|— 1/1 LinearLayout 2129243036
|— 01/74 RelativeLayout 2131296449
|— 1/4 MaterialCheckBox 2131296332
|— 2/4 MaterialTextView 2131296547
|— 3/4 MaterialTextView 2131296531
|— 4/4 AppCompatImageButton 2131296462
|— 02/74 RelativeLayout 2131296449
|— 1/4 MaterialCheckBox 2131296332
|— 2/4 MaterialTextView 2131296547
|— 3/4 MaterialTextView 2131296531
|— 4/4 AppCompatImageButton 2131296462
|— ...
|— 74/74 RelativeLayout 2131296449
|— 1/4 MaterialCheckBox 2131296332
|— 2/4 MaterialTextView 2131296547
|— 3/4 MaterialTextView 2131296531
|— 4/4 AppCompatImageButton 2131296462
|— 2/4 MaterialTextView 2131296445
|— 3/4 FloatingActionButton 2131296374
|— 4/4 MaterialTextView 2131296363

These Views are not mine, they are coming from other Apps, so I cannot touch code related to them (I'm inflating them from a RemoteView)
If you are receiving a RemoteViews, and you are applying it to something in your activity, you have direct access to the resulting Views, no different than if you had inflated them from XML yourself. Given the root View, it is simply a matter of walking the children (probably depth-first, but that's your call) and logging whatever information you want.

Just find menu item from BottomNavigationView and register long click.
View itemBag = bottomNavigationView.findViewById(R.id.action_bag);
if (itemBag != null) {
itemBag.setOnLongClickListener(BottomNavigationActivity.this::onLongClick);
}
private boolean onLongClick(View v) {
switch (v.getId()) {
case R.id.action_bag: {
showToastMessage(R.string.menu_shopping_bag);
}
break;
}
return false;
}

Related

Deleting child view from first position and adding it to last position

I have a custom view called TinderStackLayout.
At a certain part of the code I am deleting a child view of it and re-adding it back at the last position possible -
private void handleViewAfterAnimation(View view, boolean shouldSkipView) {
Log.d("card view - ", "inside handleViewAfterAnimation");
isCardAnimating = false;
TinderStackLayout tinderStackLayout = (TinderStackLayout) view.getParent();
if (tinderStackLayout == null)
return;
tinderStackLayout.removeView(view);
if (shouldSkipView)
tinderStackLayout.addCard((TinderCardView) view, tinderStackLayout.getChildCount() - 1);
//this part is for debugging purpose
for (int i = 0; i < tinderStackLayout.getChildCount(); i++) {
View childAt = tinderStackLayout.getChildAt(i);
if (childAt instanceof TinderCardView)
Log.d("card view - ", "child cards after deletion - " + (((TinderCardView) childAt).usernameTextView.getText()));
}
}
here is my addCard() method -
public void addCard(TinderCardView tinderCardView, int addToPosition) {
View topCard = getChildAt(0);
if (topCard != null && topCard.equals(tinderCardView)) {
return;
}
topCardOnStack = tinderCardView;
ViewGroup.LayoutParams layoutParams;
layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
addView(tinderCardView, addToPosition, layoutParams);
// tinderCardView.animate()
// .x(0)
// .setInterpolator(new AnticipateOvershootInterpolator());
}
What I don't understand is what I get in the UI -
I have 3 cards.
I press the button, one card is being animated away, the second one is being shown. I press the button again, the second one animates away. I press the last one and the button does not work anymore. What I want to achieve is the first card appearing now behind the last one.
Here is what I get when logging the values out -
seems correct, it was before clicking 3 2 1 and now 2 3 1. The next one should be 1 2 3, but this is what I get for the next one -
goes back to 3 2 1 instead of 1 2 3. I can't figure out why. ?
Edit:
found the reason why this is happening, I am giving a view which should always be the top card on the stack but I am actually not giving the top card because I am always adding a new card. Here is the method -
public void handleButtonPressed(int buttonTag) {
Log.d("card view - ", "inside handleButtonPressed");
TinderStackLayout tinderStackLayout = ((TinderStackLayout) this.getParent());
TinderCardView topCard = (TinderCardView) tinderStackLayout.getChildAt(tinderStackLayout.getChildCount() - 1);
if (isCardAnimating) {
return;
}
switch (buttonTag) {
case DELETE_BUTTON_PRESSED:
isCardAnimating = true;
deleteCard(topCard);
break;
case PASS_BUTTON_PRESSED:
Log.d("card view - ", "inside pass button pressed");
isCardAnimating = true;
passCard(topCard);
Log.d("card view - ", "top card Value before pass - " + topCard.displayNameTextView.getText());
Log.d("card view - ", "child count - " + tinderStackLayout.getChildCount());
for (int i = 0; i < tinderStackLayout.getChildCount(); i++) {
View childAt = tinderStackLayout.getChildAt(i);
if (childAt instanceof TinderCardView)
Log.d("card view - ", "child cards before deletion - " + (((TinderCardView) childAt).usernameTextView.getText()));
}
break;
case APPROVE_BUTTON_PRESSED:
showLawyerContactDetailsFragment(topCard);
break;
}
}
I am trying to do TinderCardView topCard = (TinderCardView) tinderStackLayout.getChildAt(tinderStackLayout.getChildCount() - 1) in order to get the top card in my stack, which would be correct if I delete the cards and not re-add them to my stack but that is not the case when re-adding them. What should be the solution for always getting the top card when I am adding new views all the time?
If you want to shuffle cards, then you don't need to delete and re-add them. You simply need a data structure, which will do it for you. For your use case you can use Circular Array.
private CircularArray cardArray; //declaration
Now when you are adding your views, add it to your card array also.
cardArray.addLast(tinderCardView);
addView(tinderCardView); // add to your layout.
Then use this code to check.
int shuffleCount = 3;
for (int i = 1; i <= shuffleCount; i++)
shuffleTopCard(i);
Finally shuffleTopCard() method.
private void shuffleTopCard(int shuffleID) {
Log.d(TAG, "shuffleTopCard: cards before shuffle");
for (int i = 0; i < cardCount; i++)
Log.d(TAG, "shuffleTopCard: " + ((TinderCardView) cardArray.get(i)).getTag());
TinderCardView cardView = (TinderCardView) cardArray.popLast();
Log.e(TAG, "shuffleTopCard: top cardID = " + cardView.getTag());
cardArray.addFirst(cardView);
Log.d(TAG, "shuffleTopCard: cards after shuffling " + shuffleID + " time");
for (int i = 0; i < cardArray.size(); i++)
Log.d(TAG, "shuffleTopCard: " + ((TinderCardView) cardArray.get(i)).getTag());
Log.e(TAG, "shuffleTopCard: after shuffle top card = " + ((TinderCardView) cardArray.getLast()).getTag());
}
Use of CircularArray will free you from head-ache of maintaining card position manually and also separates your CustomViewGroup and card shuffling logic thereby resulting in loosely-coupled code.

getBackground() return null

So I'm trying to hook OnActivityResume using Xposed. This is one part of my code in after hook:
ArrayList<View> views = new ArrayList<View>();
listViews(views, rootView);
Log.d("ViewListing", Integer.toString(views.size()));
for(Iterator<View> i = views.iterator(); i.hasNext(); ) {
final View item = i.next();
Log.d("ViewListing", "View:" + item.getClass().getName() + ":" + Boolean.toString(item instanceof android.support.v7.widget.Toolbar));
if(item.getClass().getName().equals("android.support.v7.widget.Toolbar")) {
Log.d("ViewListing", "Found Toolbar!");
Drawable d = (Drawable) XposedHelpers.getObjectField(item, "mBackground");
Log.d("ViewListing", "And background?" + d);
}
}
The toolbar is found, but the drawable is null. (My goal is to get the background color)

Android : How to get Dynamic EditText values

In my application I'am creating 10 EditText by dynamically. Now I want to give different value in run time and I want to add it to the list. I have assigned EditText object to the String variable like object.getText.toString(). But i cant get any value.I'am a beginner in android. Can anyone help me how to achieve this? Thanks in advance.
for(int i=0;i<=10;i++)
{
requirement = require.get(i);
RelativeLayout rl1 = new RelativeLayout(getActivity());
rl1.addView(req1);
req1estimate_value = new EditText(getActivity());
String value = req1estimate_value.getText().toString();
rl2.addView(req1estimate_value);
}
Try this. You should instantiate relative layout (rl1) at out of for loop, and should add child views with in that, so that all views could belongs to a parent layout. After that for accessing the values of all EditText you can use following:
String viewValue;
ViewGroup rootView = (ViewGroup) rl1;
int count = rootView.getChildCount();
for (int i = 0; i < count; i++) {
View view = rootView.getChildAt(i);
if (view instanceof EditText) {
viewValue = ((EditText) view).getText().toString();
Log.v("Value:: ", i + " " + viewValue);
} else if (view instanceof Spinner) {
viewValue = ((Spinner) view).getSelectedItem()
.toString();
Log.v("Value:: ", i + " " + viewValue);
}
}
Now after getting values you can put on a List or anywhere you want to use.

Dynamic numberpicker android does not work

I'm looking for instantiate dynamically a numberpicker. I want to place a value (kitQty - YourQty) in numberpicker order.
The next code is now working but only for the last one Id=9.
Because this only working with the last record?
Try setTag and setId and I can not change the numberpicker that desire. Anyone know what may be wrong in my code?
Thank you very much!
public class MyKits extends Fragment {
private Button performAudit;
private Button submitOrder;
NumberPicker yourQty;
NumberPicker kitQty;
NumberPicker order;
Conexion basedatos;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_mykits, container, false);
Context context=v.getContext();
basedatos=new Conexion(v.getContext());
TextView tvModel = (TextView) v.findViewById(R.id.tvModel);
TableLayout table= (TableLayout) v.findViewById(R.id.Tabla);
tvModel.setText(getArguments().getString("model"));
for(int i=0;i<10;i++)
{
TableRow tR = new TableRow(v.getContext());
LinearLayout l1= new LinearLayout(v.getContext());
LinearLayout l2= new LinearLayout(v.getContext());
LinearLayout l3= new LinearLayout(v.getContext());
LinearLayout l4= new LinearLayout(v.getContext());
Button item = new Button(v.getContext());
kitQty = new NumberPicker(getActivity().getApplicationContext());
yourQty= new NumberPicker(getActivity().getApplicationContext());
order= new NumberPicker(getActivity().getApplicationContext());
item.setText("Prueba Imparjdsbfjbdsjfgbijsdfgijnsfdignsidfgh");
l1.addView(kitQty);
l2.addView(item);
l3.addView(yourQty);
l4.addView(order);
///Tamaño Texto
item.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
kitQty.setMaxValue(100);
kitQty.setValue(50);
kitQty.setMinValue(0);
kitQty.setEnabled(false);
order.setMinValue(0);
order.setMaxValue(kitQty.getMaxValue());
yourQty.setMaxValue(kitQty.getMaxValue());
yourQty.setMinValue(0);
yourQty.setTag(i);
yourQty.setId(i);
order.setTag(i);
order.setId(i);
kitQty.setTag(i);
kitQty.setId(i);
yourQty.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
#Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
Log.e("oldValue: " + oldVal + " ID: " + picker.getId(), " newVal: " + newVal + " ID: " + picker.getTag());
kitQty.setId(picker.getId());
yourQty.setId(picker.getId());
order.setId(picker.getId());
order.setValue(kitQty.getValue() - yourQty.getValue());
}
});
order.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
#Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
Log.e("oldValue: " + oldVal + " ID: " + picker.getId(), " newVal: " + newVal + " ID: " + picker.getTag());
kitQty.setTag(picker.getTag());
yourQty.setTag(picker.getTag());
order.setTag(picker.getTag());
order.setValue(kitQty.getValue() - yourQty.getValue());
// calcularQty(picker);
}
});
tR.addView(l1);
tR.addView(l2);
tR.addView(l3);
tR.addView(l4);
table.addView(tR);
}
return v;}
public static MyKits newInstance(String model) {
MyKits f = new MyKits();
Bundle b = new Bundle();
b.putString("model", model);
f.setArguments(b);
return f;} }
Well, there are a few things wrong in your code...
But let's just focus on the main question in saying first that you should not assign id by yourself. I'd rather either inflate each of them with a granular xml layout file, or if you insist on creating your widgets using "new", then leave the id alone and Android should assign them properly.
But really the main reason why your code don't work is that you assign widget ids in a "for" loop with your counter variable (i) and you are using the same value as an id for order widget, yourQty widget and kitQty widget. You need to understand that each widget id must be unique in the same view hierarchy. First start by fixing this issue. But I won't tell you how: you should be able to fix this by yourself.

Handling images in HTML In TextView in a different way

I'm trying to show Html in TextView. And all works fine, but I want to display images in TextView in different way.
For example, I want each image to be in the container, which can be dragged and dropped.
Does anyone knows any way to implement this?
Thanks!
Upd: I can't use WebView, because I will show about 10-20 separate views on same Activity at once. And I don't think it will help me to implement this.
This is how I solved the problem.
The main idea:
Go through all spans from Html.fromHtml(section.text) and search for ImageSpan. Once it is found set all previous spans as text for one TextView, create ImageView for image, continue searching.
Code:
new AsyncTask<String, Integer, List<Object>>() {
#Override
protected List<Object> doInBackground(String... params) {
List<Object> res = new ArrayList<Object>();
Spanned in = Html.fromHtml(section.text);
Object[] spans = in.getSpans(0, Integer.MAX_VALUE, Object.class); // get all spans
int lastImageSpanPosition = 0; // it's end position of image span
for (int i = 0; i < spans.length; i++) { // itarete searching ImageSpan
Object span = spans[i];
if (span instanceof ImageSpan) {
int spanStart = in.getSpanStart(span); // If you;ve found one
if (lastImageSpanPosition == spanStart)
continue; // check if image is first span (avoid creation of empty spans).
res.add(new SpannableStringBuilder(in.subSequence(lastImageSpanPosition, spanStart))); // add all previous spans as a single Spannable object
ImageSpan imageSpan = (ImageSpan) span;
String imageUrl = imageSpan.getSource();
if (!imageUrl.startsWith("http"))
imageUrl = "http:" + imageUrl;
res.add(new ImageSpan(null, imageUrl)); // add separate span for image
lastImageSpanPosition = in.getSpanEnd(span);
}
if (i < spans.length - 1 && !containsImageSpan(spans, i + 1)) { // to not lose text in the end
res.add(new SpannableStringBuilder(in.subSequence(lastImageSpanPosition, in.getSpanEnd(spans[spans.length - 1]))));
break;
}
}
return res;
}
#Override
protected void onPostExecute(List<Object> objects) {
for (Object object : objects) {
View v = null;
if (object instanceof ImageSpan) { // create separate views for each span
NetworkImageView networkImageView = new NetworkImageView(getContext());
networkImageView.setImageUrl(((ImageSpan) object).getSource(), App.get().getImageLoader());
v = networkImageView;
} else {
TextView textView = new TextView(getContext());
textView.setText((CharSequence) object);
v = textView;
}
holder.sectionTextViewsContainer.addView(v);
}
}
}.execute(section.text);
EDIT: added containsImageSpan method
private boolean containsImageSpan(Object[] spans, int index) {
for (int i = index; i < spans.length; i++) {
if (spans[i] instanceof ImageSpan) {
return true;
}
}
return false;
}
According to the documentation of HTML class, I'm not sure you can achieve what you want. Take a look at
http://developer.android.com/reference/android/text/Html.html
If you really want to use HTML I recommand you to use a webview. Otherwise, you can use native drag and drop. Check the official training: http://developer.android.com/guide/topics/ui/drag-drop.html

Categories

Resources