Android: ScrollView's child custom view not getting proper height - android

I'm writing a custom view, which is having a ScrollView as a parent, because, the custom view height exceeds the screen's height. The problem that I'm facing is that I cannot scroll the layout.
The fragment which is added in the activity's UI:
public class FragmentHour extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
HourView view = new HourView(getActivity());
ScrollView sv = new ScrollView(getActivity());
sv.setFillViewport(true);
sv.addView(view);
return sv;
}
}
The custom view:
public class HourView extends View {
// Default rectangle for one row, used to know the sizes
private Rect mDefRect;
// Paint to draw the lines
private Paint mPaint;
public HourView(Context context) {
super(context);
init();
}
public HourView(Context context, AttributeSet set) {
super(context, set);
init();
}
public HourView(Context context, AttributeSet set, int defStyle) {
super(context, set, defStyle);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.GRAY);
mPaint.setStrokeWidth(UIUtils.dpToPx(getContext(), 0.5f));
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Create a default rectangle, for one row
// We need to recreate the rectangle multiple times, because getWidth() == 0
int height = UIUitls.dpToPx(getContext(), 50); // dpi to px
mDefRect = new Rect(0, 0, getWidth(), height);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
int verticalOffset = 0;
for (int i = 0; i < 24; i++) {
int cellHeight = mDefRect.height();
verticalOffset += cellHeight;
canvas.drawLine(0, verticalOffset, getWidth(), verticalOffset, mPaint);
}
}
}
The code above will result:
As I said, the problem is that I cannot scroll the view, probably because the child view's height, it's the size of it's parent.
I have tried to override the onMeasure method:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDefRect != null) {
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
int newHeight = mDefRect.height() * 24;
setMeasuredDimension(measuredWidth, newHeight);
// super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY));
}
else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
I have tried to register for layout-change, then change the Layout parameters:
private void init() {
// ...
post(new Runnable() {
#Override
public void run() {
getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#SuppressLint("NewApi")
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
getLayoutParams().height = mDefRect.height() * 24;
// postInvalidate();
}
});
}
});
}

Related

custom view not getting drawn in custom viewgroup

I have a custom view class that draws a rectangle and I have a custom view group where I am trying to add these custom views but somehow the views are not getting drawn properly(at times only one of the view gets drawn).
Not able to locate the problem!
MainActivity.java
public class MainActivity extends Activity {
private CustomTaskView customTaskView;
private FrameLayout frameLayout;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
customTaskView = new CustomTaskView(MainActivity.this);
frameLayout = new FrameLayout(MainActivity.this);
frameLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
Paint blackPaint = new Paint();
blackPaint.setColor(Color.BLACK);
blackPaint.setStyle(Paint.Style.STROKE);
CustomView customView1 = new CustomView(MainActivity.this, 100, 50, 100, 300, blackPaint);
customView1.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
frameLayout.addView(customView1);
Paint redPaint = new Paint();
redPaint.setColor(Color.RED);
redPaint.setStyle(Paint.Style.STROKE);
CustomView customView2 = new CustomView(MainActivity.this, 200, 50, 300, 400, redPaint);
customView2.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
frameLayout.addView(customView2);
setContentView(frameLayout);
}
}
CustomView.java
public class CustomView extends View {
private Paint paint;
private float l, t, r, b;
public CustomView(Context context, float l, float t, float r, float b, Paint paint) {
super(context);
this.b = b;
this.l = l;
this.r = r;
this.t = t;
this.paint = paint;
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(l, t, r, b, paint);
}
}
CustomTaskView.java(the custom viewgroup file)
public class CustomTaskView extends ViewGroup implements LongPressGestureListener.HandleClicks {
int width, height;
private GestureDetectorCompat mGestureDetector;
private LongPressGestureListener longPressGestureListener;
private Map<Integer, List<Point>> map;
public CustomTaskView(Context context) {
super(context);
longPressGestureListener = new LongPressGestureListener(context, CustomTaskView.this);
mGestureDetector = new GestureDetectorCompat(context, longPressGestureListener);
map = new HashMap<>();
}
public CustomTaskView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
// Handle any other event here, if not long press.
return true;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
#Override
public void handleLongClick(MotionEvent event) {
Log.e("ganesh", "handling long clicks!");
}
#Override
public void handleSingleClick(MotionEvent event) {
float xCoordinate = event.getX();
float yCoordinate = event.getY();
Point tempPoint = new Point(xCoordinate, yCoordinate);
int count = 0;
for (Map.Entry<Integer, List<Point>> entry : map.entrySet()) {
if (isPointInside(entry.getValue(), tempPoint))
count++;
}
Log.e("ganesh", "handling single clicks!" + xCoordinate + " " + yCoordinate + " count: " + count);
}
public boolean isPointInside(List<Point> pointList, Point targetPoint) {
return targetPoint.getxCoordinate() >= pointList.get(0).getxCoordinate() && targetPoint.getxCoordinate() <= pointList.get(1).getxCoordinate() && targetPoint.getyCoordinate() >= pointList.get(0).getyCoordinate() && targetPoint.getyCoordinate() <= pointList.get(2).getyCoordinate();
}
}
Whenever you make any change to a custom view, you need to rebuild your project for the XML preview parser to draw. Try Build->Rebuild Project once.
You have to provide the correct implementation of onLayout and onMeasure method in your custom view if you extend ViewGroup class directly. You can find out more about these two methods here Google Developers Guide Custom ViewGroup
Well here is the edit to your current CustomTaskView.java to make your code work.
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
CustomView customView = (CustomView) this.getChildAt(i);
customView.layout(l, t, r, b);
}
}
In Custom View onLayout method is responsible for Position all children within the layout. You need to call layout method on all the children and provide calculated left, top, right, bottom attribute to them.

Rectangle draw call in mainActivity

I draw a rectangle in the activity_main folder and I get these values to be drawn in integer.xml, but I do not want to do that.getInteger (R.integer.scanner_rect_width and getResources (). getInteger (R.integer.scanner_rect_height I get these values from the integer.xml folder, but how do I assign these values by creating a mainActivity object?
ScannerOverlay scannerOverlay = new ScannerOverlay (); as
ScanerOverlay.java
public class ScannerOverlay extends ViewGroup {
private float left, top, endY;
private int rectWidth, rectHeight;
private int frames;
private boolean revAnimation;
private int lineColor, lineWidth;
public ScannerOverlay(Context context) {
super(context);
}
public void setWidth(int pixels) {
rectWidth = pixels;
requestLayout();
invalidate();
}
public void setHeight(int pixels) {
rectHeight = pixels;
requestLayout();
invalidate();
}
public ScannerOverlay(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScannerOverlay(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.ScannerOverlay,
0, 0);
rectWidth = a.getInteger(R.styleable.ScannerOverlay_square_width, 150);
rectHeight = a.getInteger(R.styleable.ScannerOverlay_square_height, 150);
lineColor = a.getColor(R.styleable.ScannerOverlay_line_color, ContextCompat.getColor(context, R.color.scanner_line));
lineWidth = a.getInteger(R.styleable.ScannerOverlay_line_width, getResources().getInteger(R.integer.line_width));
frames = a.getInteger(R.styleable.ScannerOverlay_line_speed, getResources().getInteger(R.integer.line_width));
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
#Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
left = (w - dpToPx(rectWidth)) / 2;
top = (h - dpToPx(rectHeight)) / 2;
endY = top;
super.onSizeChanged(w, h, oldw, oldh);
}
public int dpToPx(int dp) {
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
}
#Override
public boolean shouldDelayChildPressedState() {
return false;
}
#Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw transparent rect
int cornerRadius = 0;
Paint eraser = new Paint();
eraser.setAntiAlias(true);
eraser.setColor(Color.WHITE);
RectF rect = new RectF(left, top, dpToPx(rectWidth) + left, dpToPx(rectHeight) + top);
canvas.drawRoundRect(rect, (float) cornerRadius, (float) cornerRadius, eraser);
// draw horizontal line
Paint line = new Paint();
line.setColor(lineColor);
line.setStrokeWidth(Float.valueOf(lineWidth));
// draw the line to product animation
if (endY >= top + dpToPx(rectHeight) + frames) {
revAnimation = true;
} else if (endY == top + frames) {
revAnimation = false;
}
// check if the line has reached to bottom
if (revAnimation) {
endY -= frames;
} else {
endY += frames;
}
canvas.drawLine(left, endY, left + dpToPx(rectWidth), endY, line);
invalidate();
}
}
activity_main.xml
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.yavuz.myapplication.MainActivity">
<com.example.yavuz.myapplication.ScannerOverlay
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#44000000"
app:line_color="#7323DC"
app:line_speed="6"
app:line_width="4"
/>
MainActivity.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
You have to create setters methods for width and height as below:
public void setWidth(int pixels) {
rectWidth = pixels;
requestLayout();
invalidate();
}
public void setHeight(int pixels) {
rectHeight = pixels;
requestLayout();
invalidate();
}
Then to use it MainActivity you use it as below:
ScannerOverlay scannerOverlay = new ScannerOverlay();
scannerOverlay.setWidth(200); // for example
scannerOverlay.setHeight(300);
setContentView(scannerOverlay);
I suppose that ScannerOverlay is a custom view.
from xml layout:
<thepackage_where_theclass.ScannerOverlay
app:scanner_rect_width="200"
app:scanner_rect_height="200"/>

Clear a views canvas with a TextView

I'm trying to achieve a visual effect, that if I could make would look awesome! The login of the app that I'm doing looks like this:
Keep in mind that the image on the background is an animation, that makes a slightly transition from that image to another.
What I want is make the title of the app "Akrasia" be transparent, but transparent meaning that you can see the image in background through the title letters, this means that in some way I must override the onDraw method of the RelativeLayout that contains this form. I tried to do that, but the only thing that I got was errors. Maybe I'm wrong trying to override the onDraw method in boths, the TextView and the RelativeLayout, maybe there's an easiest way to do it. What do you think? Or maybe is impossible to achive this effect?
UPDATE:
This is how it should look like.
Also I tried to make a custom view extending from TextView wich has a method setBackgroundView wich stores a view instance into a field. Later on the onDraw method and I managed to get the bitmap from the background image. But I don't know how draw it using canvas.
UPDATE:
I make it work! Now I only need change that blue-like background by the drawable of the background.
The view:
final public class SeeThroughTextView extends TextView
{
Bitmap mMaskBitmap;
Canvas mMaskCanvas;
Paint mPaint;
Drawable mBackground;
Bitmap mBackgroundBitmap;
Canvas mBackgroundCanvas;
boolean mSetBoundsOnSizeAvailable = false;
public SeeThroughTextView(Context context)
{
super(context);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
}
public SeeThroughTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SeeThroughTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
#Override
#Deprecated
public void setBackgroundDrawable(Drawable bg)
{
mBackground = bg;
int w = bg.getIntrinsicWidth();
int h = bg.getIntrinsicHeight();
// Drawable has no dimensions, retrieve View's dimensions
if (w == -1 || h == -1)
{
w = getWidth();
h = getHeight();
}
// Layout has not run
if (w == 0 || h == 0)
{
mSetBoundsOnSizeAvailable = true;
return;
}
mBackground.setBounds(0, 0, w, h);
invalidate();
}
#Override
public void setBackgroundColor(int color)
{
setBackgroundDrawable(new ColorDrawable(color));
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
mBackgroundBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mBackgroundCanvas = new Canvas(mBackgroundBitmap);
mMaskBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mMaskCanvas = new Canvas(mMaskBitmap);
if (mSetBoundsOnSizeAvailable)
{
mBackground.setBounds(0, 0, w, h);
mSetBoundsOnSizeAvailable = false;
}
}
#Override
protected void onDraw(Canvas canvas)
{
// Draw background
mBackground.draw(mBackgroundCanvas);
// Draw mask
mMaskCanvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
super.onDraw(mMaskCanvas);
mBackgroundCanvas.drawBitmap(mMaskBitmap, 0.f, 0.f, mPaint);
canvas.drawBitmap(mBackgroundBitmap, 0.f, 0.f, null);
}
}
And in my fragment I have this because the animation in the background:
vBackground.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) {
vTitle.setBackgroundDrawable(new BitmapDrawable(vBackground.getDrawingCache()));
vTitle.invalidate();
}
});
Nailed!
The view:
final public class SeeThroughTextView extends TextView
{
Bitmap mMaskBitmap;
Canvas mMaskCanvas;
Paint mPaint;
Drawable mBackground;
Bitmap mBackgroundBitmap;
Canvas mBackgroundCanvas;
boolean mSetBoundsOnSizeAvailable = false;
public SeeThroughTextView(Context context)
{
super(context);
init();
}
private void init() {
Typeface myTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/gillsans.ttf");
setTypeface(myTypeface);
mPaint = new Paint();
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
}
public SeeThroughTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SeeThroughTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
#Override
#Deprecated
public void setBackgroundDrawable(Drawable bg)
{
mBackground = bg;
int w = bg.getIntrinsicWidth();
int h = bg.getIntrinsicHeight();
// Drawable has no dimensions, retrieve View's dimensions
if (w == -1 || h == -1)
{
w = getWidth();
h = getHeight();
}
// Layout has not run
if (w == 0 || h == 0)
{
mSetBoundsOnSizeAvailable = true;
return;
}
mBackground.setBounds(0, 0, w, h);
invalidate();
}
#Override
public void setBackgroundColor(int color)
{
setBackgroundDrawable(new ColorDrawable(color));
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
mBackgroundBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mBackgroundCanvas = new Canvas(mBackgroundBitmap);
mMaskBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mMaskCanvas = new Canvas(mMaskBitmap);
if (mSetBoundsOnSizeAvailable)
{
mBackground.setBounds(0, 0, w, h);
mSetBoundsOnSizeAvailable = false;
}
}
#Override
protected void onDraw(Canvas canvas)
{
// Draw background
mBackground.draw(mBackgroundCanvas);
// Draw mask
mMaskCanvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
super.onDraw(mMaskCanvas);
mBackgroundCanvas.drawBitmap(mMaskBitmap, 0.f, 0.f, mPaint);
canvas.drawBitmap(mBackgroundBitmap, 0.f, 0.f, null);
}
}
In my fragment:
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
vLoginBtn = (Button) view.findViewById(R.id.btn_login);
vRegistrationBtn = (Button) view.findViewById(R.id.btn_registration);
vForgotBtn = (Button) view.findViewById(R.id.btn_forgot);
vBackground = (KenBurnsView) view.findViewById(R.id.login_background);
vTitle = (SeeThroughTextView) view.findViewById(R.id.txt_view_login_title);
vBackground.setResourceUrls(
"http://www.youwall.com/papel/peaceful_place_wallpaper_4f3f3.jpg",
"http://www.fwallpaper.net/wallpapers/P/E/Peaceful-Scenary_1920x1200.jpg",
"http://p1.pichost.me/i/39/1620902.jpg"
);
vBackground.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) {
vTitle.setBackgroundDrawable(getResources().getDrawable(R.drawable.drawable_background_login_top));
vTitle.invalidate();
vBackground.removeOnLayoutChangeListener(this);
}
});
}
The drawables are just two shapes, one with the top-left corner and top-right corner with radius 10dp and the another one with the radius in the bottoms.
The custom TextView with the top drawable shape is alligned above the RelativeLayout wich contains the EditTexts.
No much rocket science. Thanks a lot to #Klotor for suggesting the idea!
Specify a new color in your res/values/colors.xml file (create one if it doesn't exist), the file might look like:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ltGray">#33999999</color>
</resources>
where the first two numbers are transparency (00 - fully transparent, FF - fully opaque).
Then simply set the text color of desired TextView to #color/ltGray in the xml of that layout, or go
tvTitle.setTextColor(getResources().getColor(R.color.ltGray))
after instatiating the TextView.

Mysterious behavior of custom ListView (drawing item on Canvas) in android 2.3.3

I'm creating custom ListView. Adapter:
public class CustomListEventsAdapter extends BaseAdapter {
private Context context;
private List<Map<String, String>> values;
public CustomListEventsAdapter(Context context, List<Map<String, String>> values) {
this.context = context;
this.values = values;
}
#Override
public int getCount() {
return values.size();
}
#Override
public Object getItem(int position) {
return values.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
final CustomItemForEventsList view = new CustomItemForEventsList(context, values.get(position), (long) position);
int itemHeight;
if (parent.getWidth() > parent.getHeight()) {
itemHeight = parent.getHeight();
} else {
itemHeight = parent.getHeight() / 2;
}
view.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, itemHeight));
return view;
}}
Custom View for ListView's item:
public class CustomItemForEventsList extends View {
private MainActivity context;
private Map<String,String> values;
private long id;
private Paint pMainText;
private int viewWidth;
private int viewHeight;
private boolean isLandscape;
private Bitmap backgroundBitmap;
private RectF bitmapSizes;
private RectF viewSizes;
private Matrix matrix;
public CustomItemForEventsList(final Context context, Map<String,String> values, long id) {
super(context);
this.context = (MainActivity) context;
this.values = values;
this.id = id;
pMainText = new Paint(Paint.ANTI_ALIAS_FLAG);
pMainText.setColor(Color.WHITE);
pMainText.setTextAlign(Paint.Align.LEFT);
bitmapSizes = new RectF();
viewSizes = new RectF();
matrix = new Matrix();
loadBackgroundBitmap(values.get("url"));
}
private void loadBackgroundBitmap(final String url) {
context.imageLoader.loadImage(url, new ImageSize(viewWidth, viewHeight), context.displayImageOptions, new ImageLoadingListener() {
#Override
public void onLoadingStarted(String s, View view) {
}
#Override
public void onLoadingFailed(String s, View view, FailReason failReason) {
// Log.d(Statics.LOG, "onLoadingFailed");
}
#Override
public void onLoadingComplete(String s, View view, Bitmap bitmap) {
backgroundBitmap = bitmap;
invalidate();
}
#Override
public void onLoadingCancelled(String s, View view) {
// Log.d(Statics.LOG, "onLoadingCanceled");
loadBackgroundBitmap(url);
}
});
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
viewHeight = MeasureSpec.getSize(heightMeasureSpec);
isLandscape = viewWidth > viewHeight;
viewSizes.set(0, 0, viewWidth, viewHeight);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawARGB(255, 0, 0, 0);
// Drawing view's background
if (backgroundBitmap != null) {
if (!backgroundBitmap.isRecycled()) {
bitmapSizes.set(0, 0, backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
matrix.setRectToRect(viewSizes, bitmapSizes, Matrix.ScaleToFit.FILL);
matrix.invert(matrix);
canvas.drawBitmap(backgroundBitmap, matrix, null);
}
}
}}
Everything works perfect on new Android's versions (tested on 4.0, 4.2, 4.4), but in 2.3.3 appears troubles with positioning and resizing of ListView items: bitmaps don't resize to view's sizes in onDraw, when we click on ListView or scroll it and release.
Screens from 2.3.3. That's what happening when we touch down ListView, then scroll, then wait for a while and release (that behavior I want to see in all cases):
http://s16.postimg.org/qvf26fb45/image.png
And here is what usually happens, when we click on ListView or scroll and release:
http://s22.postimg.org/d7fd8quc1/image.png
Why does it happen and how can we fix that?
Ok, I found out how to solve that problem in Android 2.3.3. Of course we can check Android's version in code and make two branches: for old and new versions. For new versions use Matrix:
if (!backgroundBitmap.isRecycled()) {
bitmapSizes.set(0, 0, backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
matrix.setRectToRect(viewSizes, bitmapSizes, Matrix.ScaleToFit.CENTER);
matrix.invert(matrix);
canvas.drawBitmap(backgroundBitmap, matrix, null);
}
And for old versions use method, proposed by #pskink in comments. But I don't want to see any differences in different Android versions (fit in old, crop in new), so I offer another way.
Our target - is to avoid operations with Rects and Matrix, which looks like are very expensive and give us some bugs with drawing custom ListView items in Android 2.3.3. So we have to do all upscale operations manually:
public class CustomItemForEventsList extends View {
private MainActivity context;
private int viewWidth;
private int viewHeight;
private Bitmap backgroundBitmap;
private Rect bitmapSizes;
private Rect preferedBitmapSizes;
public CustomItemForEventsList(final Context context, Map<String,String> values, long id) {
super(context);
this.context = (MainActivity) context;
bitmapSizes = new Rect();
preferedBitmapSizes = new Rect();
loadBackgroundBitmap(values.get("url"));
}
private void loadBackgroundBitmap(final String url) {
context.imageLoader.loadImage(url, new ImageSize(viewWidth, viewHeight), context.displayImageOptions, new ImageLoadingListener() {
#Override
public void onLoadingStarted(String s, View view) {
}
#Override
public void onLoadingFailed(String s, View view, FailReason failReason) {
// Log.d(Statics.LOG, "onLoadingFailed");
}
#Override
public void onLoadingComplete(String s, View view, Bitmap bitmap) {
backgroundBitmap = bitmap;
invalidate();
}
#Override
public void onLoadingCancelled(String s, View view) {
// Log.d(Statics.LOG, "onLoadingCanceled");
loadBackgroundBitmap(url);
}
});
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
viewHeight = MeasureSpec.getSize(heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawARGB(255, 0, 0, 0);
// Drawing view's background
if (backgroundBitmap != null) {
if (!backgroundBitmap.isRecycled()) {
bitmapSizes.set(0, 0, backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
setPreferedBitmapSizes();
canvas.drawBitmap(backgroundBitmap, bitmapSizes, preferedBitmapSizes, null);
}
}
}
private void setPreferedBitmapSizes() {
int instBmpWidth = backgroundBitmap.getWidth();
int instBmpHeight = backgroundBitmap.getHeight();
int newBmpWidth, newBmpHeight;
newBmpWidth = viewWidth; newBmpHeight = viewWidth * instBmpHeight / instBmpWidth;
if (viewHeight > newBmpHeight) {
newBmpWidth = instBmpWidth * viewHeight / instBmpHeight; newBmpHeight = viewHeight;
if (newBmpWidth == viewWidth) {
preferedBitmapSizes.set(0, 0, newBmpWidth, newBmpHeight);
} else {
preferedBitmapSizes.set(-((newBmpWidth-viewWidth)/2), 0, viewWidth + ((newBmpWidth-viewWidth)/2), newBmpHeight);
}
} else {
if (newBmpHeight == viewHeight) {
preferedBitmapSizes.set(0, 0, newBmpWidth, newBmpHeight);
} else {
preferedBitmapSizes.set(0, -((newBmpHeight-viewHeight)/2), newBmpWidth, viewHeight + ((newBmpHeight-viewHeight)/2));
}
}
}}
Now the problem is solved:
http://s29.postimg.org/hbv9d0z3b/image.png
UPDATE: We can use much more simpler code (thanks to pskink, it is his solution):
m.reset();
float scaleX = getWidth() / (float) b.getWidth();
float scaleY = getHeight() / (float) b.getHeight();
float scale = Math.max(scaleX, scaleY);
m.postScale(scale, scale);
float dx = (getWidth() - b.getWidth() * scale) / 2;
float dy = (getHeight() - b.getHeight() * scale) / 2;
m.postTranslate(dx, dy);
canvas.drawBitmap(b, m, null);
Where m - Matrix, b - our Bitmap.

Android custom view Bitmap memory leak

I've got a custom view in which I need to draw two bitmaps, one is a background, representing the image of a map and one is a pin which will be drawn on top/left position in canvas.
The both images are drawn onDraw and remain the same during the live of the activity that contains the view. After a while I get a
OutOfMemoryError: bitmap size exceeds
VM budget
This means that I have a leak and the bitmaps don't get garbage collected. I asked this question before, but now the situation changed a little. I made a init method where I set the bitmaps I want to use but this still isn't a good approach, the error appears later, but still there.
Here is the code
public class MyMapView extends View {
private int xPos = 0;
private int yPos = 0;
private int space = 0;
private Bitmap resizedBitmap;
private Bitmap position;
private Bitmap mapBitmap;
public void setMapBitmap(Bitmap value) {
this.mapBitmap = value;
}
public MyMapView(Context context) {
super(context);
}
public MyMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void init(final Context context) {
Paint paint = new Paint();
paint.setFilterBitmap(true);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
resizedBitmap = Bitmap.createScaledBitmap(mapBitmap, height, height, true);
position = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.position);
space = (width - resizedBitmap.getWidth()) / 2;
}
public void destroy()
{
resizedBitmap.recycle();
resizedBitmap=null;
position.recycle();
position=null;
mapBitmap.recycle();
mapBitmap=null;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
#Override
protected void onDraw(Canvas canvas) {
if (mapBitmap != null) {
canvas.drawBitmap(resizedBitmap, space, 0, null);
}
if (xPos != 0 && yPos != 0) {
canvas.translate(xPos + space - position.getWidth() / 2, yPos - position.getHeight() / 2);
canvas.drawBitmap(position, new Matrix(), new Paint());
}
}
public void updatePosition(int xpos, int ypos)
{
xPos = xpos;
yPos = ypos;
invalidate();
}
}
I call the setMapBitmap() and init() on onCreate of the activity that contains the view. In there I know what bitmap will be used as map. onPause of the activity I call the destroy() of the view. I still get errors.
I've read this this but I don't know how to adapt it on my case
I AM OPEN TO ANY OTHER SOLUTION. Please note that I need to resize the map bitmap (based on the height of the layout that contains it in mai activity) and still be able to draw the position on correct place.
You obviously have a memory leak. Read how to avoid memory leaks and how to find memory leaks.
Here is a your code refactored to use WeakReference:
public class MyMapView extends View {
private int xPos = 0;
private int yPos = 0;
private int space = 0;
private WeakReference<Bitmap> resizedBitmap;
private WeakReference<Bitmap> position;
private WeakReference<Bitmap> mapBitmap;
public void setMapBitmap(Bitmap value) {
this.mapBitmap = new WeakReference<Bitmap>(value);
}
public MyMapView(Context context) {
super(context);
}
public MyMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void init(Bitmap mapBitmap) {
Paint paint = new Paint();
paint.setFilterBitmap(true);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
resizedBitmap = new WeakReference<Bitmap>(Bitmap.createScaledBitmap(
mapBitmap, width, height, true));
position = new WeakReference(BitmapFactory.decodeResource(
getContext().getResources(), R.drawable.position));
space = (width - resizedBitmap.get().getWidth()) / 2;
}
// public void destroy() {
// resizedBitmap.recycle();
// resizedBitmap = null;
// position.recycle();
// position = null;
// mapBitmap.recycle();
// mapBitmap = null;
// }
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
}
#Override
protected void onDraw(Canvas canvas) {
if (mapBitmap != null) {
canvas.drawBitmap(resizedBitmap.get(), space, 0, null);
}
if (xPos != 0 && yPos != 0) {
canvas.translate(xPos + space - position.get().getWidth() / 2,
yPos - position.get().getHeight() / 2);
canvas.drawBitmap(position.get(), new Matrix(), null);
}
}
public void updatePosition(int xpos, int ypos) {
xPos = xpos;
yPos = ypos;
invalidate();
}
}
Something that marked me in your code is that you don't recycle all your bitmaps apparently. Here are a couple of code snippets that could help:
bmp = getBitmapFromRessourceID(R.drawable.menu);
ratio = (float)this.height/(float)bmp.getScaledHeight(canvas);
temp = Bitmap.createScaledBitmap(bmp, (int)(bmp.getWidth()*ratio), (int) (bmp.getHeight()*ratio-10), false);
bmp.recycle();
bmp = temp;
and:
Bitmap temp = BitmapFactory.decodeResource(context.getResources(), resource, opts);
Bitmap bmp = Bitmap.createBitmap(temp, 0, 0, temp.getWidth(), temp.getHeight(), flip, true);
temp.recycle();

Categories

Resources