In Challenge 2: Global variable does not assign


#1

I face problem in Challenge 2 and I cannot figure it out.
To solve this Challenge, I declare global variable as:

public class CrimeFragment extends Fragment {
private int mPhotoWidth;
private int mPhotoHeight;
. . .

Then, I use ViewTreeObserver.OnGlobalLayoutListener() in onCreateView as:

. . .
ViewTreeObserver observer = mPhotoView.getViewTreeObserver();
		if (observer.isAlive()) {
			observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
				@Override
				public void onGlobalLayout() {
					mPhotoView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                                 // mPhotoWidth = mPhotoView.getMeasuredWidth();
                                 // mPhotoHeight = mPhotoView.getMeasuredHeight();

					setPhotoWidth(mPhotoView.getMeasuredWidth());
					setPhotoHeight(mPhotoView.getMeasuredHeight());

					Log.d("PictureMeasurements", "onGlobalLayout: mPhotoWidth = " + mPhotoWidth);
					Log.d("PictureMeasurements", "onGlobalLayout: mPhotoHeight = " + mPhotoHeight);
				}
			});

			Log.d("PictureMeasurements", "After onGlobalLayout: mPhotoWidth = " + mPhotoWidth);
			Log.d("PictureMeasurements", "After onGlobalLayout: mPhotoHeight = " + mPhotoHeight);
		}

    updatePhotoView();

    return v;
}

and here is the setters (for debug purpose):

public void setPhotoWidth(int photoWidth) {
    mPhotoWidth = photoWidth;
}

public void setPhotoHeight(int photoHeight) {
    mPhotoHeight = photoHeight;
}

I use Log.d to check mPhotoWidth and mPhotoHeight inside and after this listener but the value return from getMeasuredWidth() (which is correct) does not assigned to the global variable.

Here is the log:

09-26 10:06:04.925 28039-28039/com.bignerdranch.android.criminalintent D/PictureMeasurements: After onGlobalLayout: mPhotoWidth = 0
09-26 10:06:04.925 28039-28039/com.bignerdranch.android.criminalintent D/PictureMeasurements: After onGlobalLayout: mPhotoHeight = 0
09-26 10:06:04.930 28039-28039/com.bignerdranch.android.criminalintent D/PictureMeasurements: onGlobalLayout: mPhotoWidth = 210
09-26 10:06:04.930 28039-28039/com.bignerdranch.android.criminalintent D/PictureMeasurements: onGlobalLayout: mPhotoHeight = 210

which cause photo to not rendering because the value in mPhotoWidth and mPhotoHeight is zero.

I cannot understand the reason, could anybody help, please?
Thanks


#2

After struggling some days :face_with_raised_eyebrow: and then writing this post, I realized the answer.

From the log I noticed that “After onGlobalLayout” is logged before “onGlobalLayout” as in the previous post. And if updatePhotoView(); is after ViewTreeObserver.OnGlobalLayoutListener(), the updatePhotoView(); is called early with invalid width and height (zeros in this case) because the global variables are not yet set.

To call updatePhotoView(); at the right time, the method should be inside ViewTreeObserver.OnGlobalLayoutListener().

Here is the complete Code:

CrimeFragment.java
package com.bignerdranch.android.criminalintent;


import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.FileProvider;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;

import java.io.File;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import static android.widget.CompoundButton.*;

/**
 * Created by behin on 8/7/2017.
 */

public class CrimeFragment extends Fragment {
	private static final String ARG_CRIME_ID = "crime_id";
	private static final String DIALOG_DATE = "DialogDate";
	private static final String DIALOG_PHOTO = "DialogPhoto";
	private static final int REQUEST_DATE = 0;
	private static final int REQUEST_CONTACT = 1;
	private static final int REQUEST_PHOTO = 2;

	private Crime mCrime;
	private File mPhotoFile;
	private EditText mTitleField;

	private Button mDateButton;
	private CheckBox mSolvedCheckBox;
	private Button mSuspectButton;
	private Button mReportButton;
	private ImageButton mPhotoButton;
	private ImageView mPhotoView;

	private int mPhotoWidth;
	private int mPhotoHeight;

	public static CrimeFragment newInstance(UUID crimeId) {
		Bundle args = new Bundle();
		args.putSerializable(ARG_CRIME_ID, crimeId);

		CrimeFragment fragment = new CrimeFragment();
		fragment.setArguments(args);
		return fragment;
	}

	@Override
	public void onCreate(@Nullable Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		UUID crimeId = (UUID) getArguments().getSerializable(ARG_CRIME_ID);
		mCrime = CrimeLab.get(getActivity()).getCrime(crimeId);
		mPhotoFile = CrimeLab.get(getActivity()).getPhotoFile(mCrime);
	}

	@Override
	public void onPause() {
		super.onPause();

		CrimeLab.get(getActivity()).updateCrime(mCrime);
	}

	@Nullable
	@Override
	public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
		View v = inflater.inflate(R.layout.fragment_crime, container, false);

		mTitleField = (EditText) v.findViewById(R.id.crime_title);
		mTitleField.setText(mCrime.getTitle());
		mTitleField.addTextChangedListener(new TextWatcher() {
			@Override
			public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

			}

			@Override
			public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
				mCrime.setTitle(charSequence.toString());
			}

			@Override
			public void afterTextChanged(Editable editable) {

			}
		});

		mDateButton = (Button) v.findViewById(R.id.crime_date);
		updateDate();
		mDateButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View view) {
				FragmentManager manager = getFragmentManager();
				DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
				dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
				dialog.show(manager, DIALOG_DATE);
			}
		});

		mSolvedCheckBox = (CheckBox) v.findViewById(R.id.crime_solved);
		mSolvedCheckBox.setChecked(mCrime.isSolved());
		mSolvedCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
			@Override
			public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
				mCrime.setSolved(b);
			}
		});

		mReportButton = (Button) v.findViewById(R.id.crime_report);
		mReportButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View view) {
				Intent i = new Intent(Intent.ACTION_SEND);
				i.setType("text/plain");
				i.putExtra(Intent.EXTRA_TEXT, getCrimeReport());
				i.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.crime_report_subject));
				i = Intent.createChooser(i, getString(R.string.send_report));
				startActivity(i);
			}
		});

		final Intent pickContact = new Intent(Intent.ACTION_PICK,
				ContactsContract.Contacts.CONTENT_URI);

		mSuspectButton = (Button) v.findViewById(R.id.crime_suspect);
		mSuspectButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View view) {
				startActivityForResult(pickContact, REQUEST_CONTACT);
			}
		});

		if (mCrime.getSuspect() != null) {
			mSuspectButton.setText(mCrime.getSuspect());
		}

		final PackageManager packageManager = getActivity().getPackageManager();
		if(packageManager.resolveActivity(pickContact, packageManager.MATCH_DEFAULT_ONLY) == null) {
			mSuspectButton.setEnabled(false);
		}

		mPhotoButton = (ImageButton) v.findViewById(R.id.crime_camera);
		final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

		boolean canTakePhoto = mPhotoFile != null &&
				captureImage.resolveActivity(packageManager) != null;
		mPhotoButton.setEnabled(canTakePhoto);

		mPhotoButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View view) {
				Uri uri = FileProvider.getUriForFile(getActivity(),
						"com.bignerdranch.android.criminalintent.fileprovider",
						mPhotoFile);
				captureImage.putExtra(MediaStore.EXTRA_OUTPUT, uri);

				List<ResolveInfo> cameraActivities = getActivity()
						.getPackageManager().queryIntentActivities(captureImage,
								packageManager.MATCH_DEFAULT_ONLY);

				for (ResolveInfo activity : cameraActivities) {
					getActivity().grantUriPermission(activity.activityInfo.packageName,
							uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
				}

				startActivityForResult(captureImage, REQUEST_PHOTO);
			}
		});

		mPhotoView = (ImageView) v.findViewById(R.id.crime_photo);
		mPhotoView.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View view) {
				FragmentManager manager = getFragmentManager();
				PhotoViewerFragment dialog = PhotoViewerFragment.newInstance(mPhotoFile);
				dialog.show(manager, DIALOG_PHOTO);
			}
		});

		ViewTreeObserver observer = mPhotoView.getViewTreeObserver();
		if (observer.isAlive()) {
			observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
				@Override
				public void onGlobalLayout() {
					mPhotoView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
					mPhotoWidth = mPhotoView.getMeasuredWidth();
					mPhotoHeight = mPhotoView.getMeasuredHeight();

					updatePhotoView();
				}
			});
		}

		return v;
	}

	@Override
	public void onActivityResult(int requestCode, int resultCode, Intent data) {
		if (resultCode != Activity.RESULT_OK) {
			return;
		}

		if (requestCode == REQUEST_DATE) {
			Date date = (Date) data.getSerializableExtra(DatePickerFragment.EXTRA_DATE);
			mCrime.setDate(date);
			updateDate();
		} else if (requestCode == REQUEST_CONTACT && data != null) {
			Uri contactUri = data.getData();
			// Specify which field you want your query to return values for
			String[] queryFields = new String[] { ContactsContract.Contacts.DISPLAY_NAME };
			// Perform your query - the contactUri is like a "where" clause here
			Cursor c = getActivity().getContentResolver()
					.query(contactUri, queryFields, null, null, null);

			try {
				// Double-check that you actually gor results
				if (c.getCount() == 0) {
					return;
				}

				// pull out the first column of the first row of data that is your suspect's name
				c.moveToFirst();
				String suspect = c.getString(0);
				mCrime.setSuspect(suspect);
				mSuspectButton.setText(suspect);
			} finally {
				c.close();
			}
		} else if (requestCode == REQUEST_PHOTO) {
			Uri uri = FileProvider.getUriForFile(getActivity(),
					"com.bignerdranch.android.criminalintent.fileprovider",
					mPhotoFile);

			getActivity().revokeUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

			updatePhotoView();
		}
	}

	private void updateDate() {
		mDateButton.setText(mCrime.getDate().toString());
	}

	private String getCrimeReport() {
		String solvedString;
		if(mCrime.isSolved()) {
			solvedString = getString(R.string.crime_report_solved);
		} else {
			solvedString = getString(R.string.crime_report_unsolved);
		}

		String dateFormat = "EEE, MMM dd";
		String dateString = DateFormat.format(dateFormat, mCrime.getDate()).toString();

		String suspect = mCrime.getSuspect();
		if (suspect == null) {
			suspect = getString(R.string.crime_report_no_suspect);
		} else {
			suspect = getString(R.string.crime_report_suspect, suspect);
		}

		String report = getString(R.string.crime_report,
				mCrime.getTitle(), dateString, solvedString, suspect);

		return report;
	}

	private void updatePhotoView() {
		if (mPhotoFile == null || !mPhotoFile.exists()) {
			mPhotoView.setImageDrawable(null);
		} else {
			Bitmap bitmap = PictureUtils.getScaledBitmap(mPhotoFile.getPath(), mPhotoWidth, mPhotoHeight);
			mPhotoView.setImageBitmap(bitmap);
		}
	}
}

Hope this helps! :crazy_face:


#3

Thanks BC2017 - I like your code.

Here’s my answer which is a little different (but not by much).

My ViewTreeObserver looks like this …

mPhotoView = (ImageView) v.findViewById(R.id.crime_photo);
ViewTreeObserver observer = mPhotoView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
updatePhotoView();
}
});

and my updatePhotoView() method looks like this.

private void updatePhotoView() {
if (mPhotoView == null || !mPhotoFile.exists()) {
mPhotoView.setImageDrawable(null);
} else {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.outHeight = mPhotoView.getMaxHeight();
options.outWidth = mPhotoView.getMaxWidth();
Bitmap bitMap = BitmapFactory.decodeFile(mPhotoFile.getPath(), options);
mPhotoView.setImageBitmap(bitMap);
}
}

My code is a little different as I interpreted the question to be that we were to write code that didn’t use the PictureUtils.getScaledBitmap(…). I could get the dimensions of mPhotoView after the layout pass and let the BitMapFactory take care of the rest. So there is no need to scale down manually by using the PictureUtils class any more.

Cheers


#4

@BC2017, you used the method getMeasuredWidth() to get the width of the Imageview

I was considering just using getWidth(). I know that will return the size in pixels, but we want them in device independent pixels (dp). So which method, if any, returns the values in dp. If none of them, then how do we convert to dp?

@cstewart, maybe you could help me out :slight_smile:


#5

Hi,

I think you are right to use getWidth() instead of getMeasuredWidth(), the answer is here stackoverflow.

Both getWidth() and getMeasuredWidth() will return size in px which I think is what you need.

dp we use it in xml file then at runtime it will be converted to px. To know the reason, see the story here medium.

You can read more info about screen densities here android developers. They provide the equation to convert dp to px and give an example when to use it.

Hope this will help.