I am trying to reconcile the following two things:
A) I want a precise, uniform, and clean UI with several identically sized buttons that correspond exactly to the underlying 'grid cells' -- A UI that will look as similar as possible (proportionally to screen size) across as many Android devices as possible.
B) On Android, the screen dimensions (aspect ratio and actual pixel numbers) of the user's device are unknown (to the app) until runtime.
My solution to this was to: (there is a code example below!)
1) Lock the app to portrait mode,
2) Do not define anything in static/absolute terms like dp,px, etc. and instead conceptualize a 'basic unit of measure' that is a function of screen height -- 0.08% in my case -- and base everything off of that.
3) Set horizontal guidelines within a ConstraintLayout whose positions are expressed as a percentage of parent (screen) height.
4) Make all buttons use this 'basic unit' as their height and width by setting their XML layout_constraintDimensionRatio attribute to "1:1" and using the guidelines above (see step 3),
5) Accomplish positioning and dimensions of all views by using constraints to either these guidelines, the parent's bounds, or one additional vertical guideline at 50% of screen width.
The problem is that depending on the pixel height of the screen (whether it happens to be odd or even... or maybe other factors), the dimensions of a view/button, (and thus the paths drawn inside it) constrained between one pair of guidelines does not exactly match those of another view drawn between some other pair... even though the distance between both pairs of guidelines should be the same percentage of parent height. :)
Here is an example showing the Nexus 4 emulator:
At first I thought the problem was simply due to rounding 'error' during Android's dimension calculations, but then why would the view not be square even though they are prescribed the 1:1 ratio attribute?
The only solutions I can think of would be:
A) To do the layout programatically instead of with XML... and set the guideline positions as exact pixel locations instead of percentages, and answer the question, "what is 0.08 x screen height?" myself... making the appropriate corrections to compensate for 'indivisible' screen heights.
B) Override onLayout() in the custom views and "force" their dimensions to be consistent... but then this would defeat the purpose of guidelines. :(
But I'm really hoping there is an easier solution than A or B.
(I know someone is going to suggest GridLayout, but it's not an option, for a few reasons... one of which is that in GridLayout, views inside cells must be set to wrap_content... which means the paths they draw cannot be generated relative to parent at runtime).
Thanks for any other suggestions, though.
Code Example:
I whipped up a simple 'minimal example' below that should be easy to reconstruct in Android Studio. The logs will reveal the issue if it's not immediately apparent.
XML layout:
<?xml version="1.0" encoding="utf-8"?>
<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:id="#+id/rootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.constraint.Guideline
android:id="#+id/guidelineHorizontalTop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.08" />
<android.support.constraint.Guideline
android:id="#+id/guidelineHorizontalBottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.92" />
<android.support.constraint.Guideline
android:id="#+id/guidelineHorizontalCenter1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.38" />
<android.support.constraint.Guideline
android:id="#+id/guidelineHorizontalCenter2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.46" />
<android.support.constraint.Guideline
android:id="#+id/guidelineHorizontalCenter3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.54" />
<android.support.constraint.Guideline
android:id="#+id/guidelineHorizontalCenter4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.62" />
<com.example.boober.stack_aliasingproblem.CustomButton
android:id="#+id/buttonTopLeft"
android:layout_width="0dp"
android:layout_height="0dp"
android:tag="buttonTopLeft"
app:layout_constraintBottom_toTopOf="#+id/guidelineHorizontalTop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.boober.stack_aliasingproblem.CustomButton
android:id="#+id/buttonTopRight"
android:layout_width="0dp"
android:layout_height="0dp"
android:tag="buttonTopRight"
app:layout_constraintBottom_toTopOf="#+id/guidelineHorizontalTop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.boober.stack_aliasingproblem.CustomButton
android:id="#+id/buttonBottomLeft"
android:layout_width="0dp"
android:layout_height="0dp"
android:tag="buttonBottomLeft"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/guidelineHorizontalBottom" />
<com.example.boober.stack_aliasingproblem.CustomButton
android:id="#+id/buttonBottomRight"
android:layout_width="0dp"
android:layout_height="0dp"
android:tag="buttonBottomRight"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#+id/guidelineHorizontalBottom" />
<com.example.boober.stack_aliasingproblem.CustomButton
android:id="#+id/buttonMiddle"
android:layout_width="0dp"
android:layout_height="0dp"
android:tag="buttonMiddle"
app:layout_constraintBottom_toBottomOf="#id/guidelineHorizontalCenter3"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#id/guidelineHorizontalCenter2" />
<com.example.boober.stack_aliasingproblem.CustomButton
android:id="#+id/buttonMiddleTopLeft"
android:layout_width="0dp"
android:layout_height="0dp"
android:tag="buttonMiddleTopLeft"
app:layout_constraintBottom_toBottomOf="#id/guidelineHorizontalCenter2"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="#id/buttonMiddle"
app:layout_constraintTop_toTopOf="#id/guidelineHorizontalCenter1" />
<com.example.boober.stack_aliasingproblem.CustomButton
android:id="#+id/buttonMiddleTopRight"
android:layout_width="0dp"
android:layout_height="0dp"
android:tag="buttonMiddleTopRight"
app:layout_constraintBottom_toBottomOf="#id/guidelineHorizontalCenter2"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toEndOf="#id/buttonMiddle"
app:layout_constraintTop_toTopOf="#id/guidelineHorizontalCenter1" />
</android.support.constraint.ConstraintLayout>
MainActivity.java:
public class MainActivity extends AppCompatActivity {
CustomButton buttonTopLeft;
CustomButton buttonTopRight;
CustomButton buttonMiddle;
CustomButton buttonMiddleTopLeft;
CustomButton getButtonMiddleTopRight;
CustomButton buttonBottomLeft;
CustomButton buttonBottomRight;
CustomButton[] arrayOfCustomButtons;
ConstraintLayout rootView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
buttonTopLeft = findViewById(R.id.buttonTopLeft);
buttonTopRight = findViewById(R.id.buttonTopRight);
buttonBottomLeft = findViewById(R.id.buttonBottomLeft);
buttonBottomRight = findViewById(R.id.buttonBottomRight);
buttonMiddle = findViewById(R.id.buttonMiddle);
buttonMiddleTopLeft = findViewById(R.id.buttonMiddleTopLeft);
getButtonMiddleTopRight = findViewById(R.id.buttonMiddleTopRight);
arrayOfCustomButtons = new CustomButton[]{buttonTopLeft, buttonTopRight, buttonBottomLeft,
buttonBottomRight, buttonMiddle, buttonMiddleTopLeft, getButtonMiddleTopRight};
rootView = findViewById(R.id.rootView);
for (final CustomButton cb : arrayOfCustomButtons) {
cb.setClickable(true);
cb.post(new Runnable() {
#Override
public void run() {
Log.i("XXX", "width of: " + cb.getTag() + " is: "
+ cb.getMeasuredWidth());
}
});
}
rootView.post(new Runnable() {
#Override
public void run() {
Log.i("XXX", "height of rootView is: " + rootView.getMeasuredHeight());
}
});
}
}
CustomButton.java:
public class CustomButton extends View {
Path myOutlinePath;
Paint myThinPaintBrush;
Paint myThickPaintBrush;
boolean isHighlighted = false;
public CustomButton(Context context) {
super(context);
init();
}
public CustomButton(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
float measuredWidth = getMeasuredWidth();
Log.i("XXX", "measured WIDTH Of " + this.getTag() + " is: " + measuredWidth);
Log.i("XXX", "measured HEIGT Of " + this.getTag() + " is: " + getMeasuredHeight());
Log.i("XXX", "\n ");
generateMyOutline(measuredWidth);
myThinPaintBrush.setStrokeWidth(measuredWidth/12);
myThickPaintBrush.setStrokeWidth(measuredWidth/6);
}
private void generateMyOutline(float W) {
Path path = new Path();
path.moveTo(0,0);
path.lineTo(W, 0);
path.lineTo(W, W);
path.lineTo(0, W);
path.lineTo(0,0);
myOutlinePath = path;
}
private void init() {
myOutlinePath = new Path();
myThinPaintBrush = new Paint();
myThinPaintBrush.setAntiAlias(false); // setting this to true does not solve the problem.
myThinPaintBrush.setStyle(Paint.Style.STROKE);
myThinPaintBrush.setStrokeCap(Paint.Cap.ROUND);
myThickPaintBrush = new Paint();
myThickPaintBrush.setAntiAlias(false);
myThickPaintBrush.setStyle(Paint.Style.STROKE);
myThickPaintBrush.setStrokeCap(Paint.Cap.ROUND);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (this.isClickable()) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isHighlighted = true;
invalidate();
break;
case MotionEvent.ACTION_UP:
isHighlighted = false;
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
isHighlighted = false;
invalidate();
break;
}
}
return super.onTouchEvent(event);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(myOutlinePath, myThinPaintBrush);
if (isHighlighted) {
canvas.drawPath(myOutlinePath, myThickPaintBrush);
}
super.onDraw(canvas);
}
}
I would go for the middle ground: Use your XML layout as is and make adjustments programmatically to the guideline positions. The following code converts percentage guidelines to fixed position guidelines by computing a new layout height that is a multiple of 8% of the height of the initial layout.
All sizes are computed correctly except for the bottom squares that tend to be larger. This can be easily corrected based upon your actual requirements (more important to be at the bottom or a certain distance from the other squares, for instance.)
MainActivity.jav
public class MainActivity extends AppCompatActivity {
CustomButton buttonTopLeft;
CustomButton buttonTopRight;
CustomButton buttonMiddle;
CustomButton buttonMiddleTopLeft;
CustomButton getButtonMiddleTopRight;
CustomButton buttonBottomLeft;
CustomButton buttonBottomRight;
CustomButton[] arrayOfCustomButtons;
ConstraintLayout rootView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
buttonTopLeft = findViewById(R.id.buttonTopLeft);
buttonTopRight = findViewById(R.id.buttonTopRight);
buttonBottomLeft = findViewById(R.id.buttonBottomLeft);
buttonBottomRight = findViewById(R.id.buttonBottomRight);
buttonMiddle = findViewById(R.id.buttonMiddle);
buttonMiddleTopLeft = findViewById(R.id.buttonMiddleTopLeft);
getButtonMiddleTopRight = findViewById(R.id.buttonMiddleTopRight);
rootView = findViewById(R.id.rootView);
rootView.post(new Runnable() {
#Override
public void run() {
int rootViewHeight = rootView.getMeasuredHeight();
Log.i("XXX", "height of rootView is: " + rootViewHeight);
int segHeight = (int) (rootViewHeight * 0.08f);
adjustGuideline(R.id.guidelineHorizontalTop, segHeight);
adjustGuideline(R.id.guidelineHorizontalCenter1, segHeight);
adjustGuideline(R.id.guidelineHorizontalCenter2, segHeight);
adjustGuideline(R.id.guidelineHorizontalCenter3, segHeight);
adjustGuideline(R.id.guidelineHorizontalCenter4, segHeight);
adjustGuideline(R.id.guidelineHorizontalBottom, segHeight);
arrayOfCustomButtons = new CustomButton[]{buttonTopLeft, buttonTopRight, buttonBottomLeft,
buttonBottomRight, buttonMiddle, buttonMiddleTopLeft, getButtonMiddleTopRight};
rootView = findViewById(R.id.rootView);
for (final CustomButton cb : arrayOfCustomButtons) {
cb.setClickable(true);
cb.post(new Runnable() {
#Override
public void run() {
Log.i("MainActivity", "<<<< width of: " + cb.getTag() + " is: "
+ cb.getMeasuredWidth());
}
});
}
}
});
}
private void adjustGuideline(int guideLineId, int segHeight) {
Guideline gl = (Guideline) findViewById(guideLineId);
ConstraintLayout.LayoutParams lp = ((ConstraintLayout.LayoutParams) gl.getLayoutParams());
gl.setGuidelineBegin((int) (segHeight * lp.guidePercent / 0.08f));
gl.setGuidelinePercent(-1f);
}
}
Related
I have a ConstraintLayout with a TextView and EditText inside. The TextView is on the left, and when the EditText gains focus, I want the TextView to change its TextSize and move to the right. When it loses focus, I want to reverse that.
This is the Layout:
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/custom_edit_text_constraint_layout"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<EditText
android:id="#+id/custom_edit_text_text_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:paddingLeft="#dimen/custom_edittext_def_hint_margin"
android:paddingStart="#dimen/custom_edittext_def_hint_margin"
tools:ignore="RtlSymmetry"
/>
<TextView
android:id="#+id/custom_edit_text_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginLeft="#dimen/custom_edittext_def_hint_margin"
android:layout_marginStart="#dimen/custom_edittext_def_hint_margin"/>
</androidx.constraintlayout.widget.ConstraintLayout>
And this is the code (If I forgot any important parts I can edit it):
hint = findViewById(R.id.custom_edit_text_hint); //TextView
textField = findViewById(R.id.custom_edit_text_text_field); //EditText
textField.setOnFocusChangeListener(new OnFocusChangeListener() {
#Override
public void onFocusChange(View v, boolean hasFocus) {
if(hasFocus) {
focusHint();
} else {
unfocusHint();
}
}
});
private void focusHint() {
hint.setTextSize(hintSizeFocused);
hint.setTextColor(hintColorFocused);
moveHintToRight();
}
private void unfocusHint() {
hint.setTextColor(hintColor);
if(textField.getText().toString().isEmpty()) {
hint.setTextSize(hintSize);
moveHintToLeft();
}
}
private void moveHintToRight() {
int horizontalDistance = textField.getWidth() - hint.getRight() - dpToPx(HINT_MARGIN_SIDE);
TranslateAnimation anim = new TranslateAnimation(0, horizontalDistance, 0, 0);
anim.setDuration(ANIMATION_DURATION);
anim.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {}
#Override
public void onAnimationEnd(Animation animation) {
ConstraintLayout l = findViewById(R.id.custom_edit_text_constraint_layout);
ConstraintSet set = new ConstraintSet();
set.clone(l);
set.clear(R.id.custom_edit_text_hint, ConstraintSet.LEFT);
set.connect(R.id.custom_edit_text_hint, ConstraintSet.RIGHT, R.id.custom_edit_text_constraint_layout, ConstraintSet.RIGHT, dpToPx(HINT_MARGIN_SIDE));
set.applyTo(l);
}
#Override
public void onAnimationRepeat(Animation animation) {}
});
hint.startAnimation(anim);
}
private void moveHintToLeft() {
int horizontalDistance = - hint.getLeft() + dpToPx(HINT_MARGIN_SIDE);
TranslateAnimation anim = new TranslateAnimation(0, horizontalDistance, 0, 0);
anim.setDuration(ANIMATION_DURATION);
anim.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {}
#Override
public void onAnimationEnd(Animation animation) {
ConstraintSet set = new ConstraintSet();
ConstraintLayout l = findViewById(R.id.custom_edit_text_constraint_layout);
set.clone(l);
set.clear(R.id.custom_edit_text_hint, ConstraintSet.RIGHT);
set.connect(R.id.custom_edit_text_hint, ConstraintSet.LEFT, R.id.custom_edit_text_constraint_layout, ConstraintSet.LEFT, dpToPx(HINT_MARGIN_SIDE));
set.applyTo(l);
}
#Override
public void onAnimationRepeat(Animation animation) {}
});
hint.startAnimation(anim);
}
This works great, but only when I don't resize the TextSize of the TextView. When I resize the TextSize (as shown in the code), hint.getLeft() and hint.getRight() return the values, which the TextView would have with the old TextSize, and this results in that the TextView moves either too far or not far enought. But this doesn't make sense to me because I resize the TextSize BEFORE I start the animation and the TextView's width is set to wrap_content. Does anyone have an idea why this doesn't work and how I can fix it?
EDIT:
To further explain and simplify what exactly the problem is, I have an example:
textView.setTextSize(12);
int width1 = hint.getWidth();
textView.setTextSize(18);
int width2 = hint.getWidth();
As the TextView's width is set to wrap_content, the width should change when I change the textSize (at least I thought so). But width1 and width2 are the same. How can I fix that?
EDIT2:
I solved the problem with
this answer by Eric.
Add right and end constraint of the textview as follows.If you want it to be in middle then set horizontal_bias = 0.5 or if you want it left then 0.0 and lastly in right 1.0
<TextView
android:id="#+id/custom_edit_text_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginLeft="#dimen/custom_edittext_def_hint_margin"
android:layout_marginStart="#dimen/custom_edittext_def_hint_margin"
android:layout_constraintRight_toRightOf = "parent"
android:layout_constraintEnd_toEndOf = "parent"
app:layout_constraintHorizontal_bias="0.0"
/>
Hope this will work.Try it
I am trying to simulate the iPhone X like navigation on Android using ViewPager. When the user releases the bar on the bottom, the viewpager's visibility is set to View.GONE and it shows the ugly drop down like animation as shown in the video https://youtu.be/k2EXKFulfzU.
I tried to remove the animation by directly changing the dimension of viewpager then changing it's visibility to GONE and it doesn't help. Thank you in advanced :)
FrameLayout ll = mLayout.findViewById(R.id.mainLL);
ll.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, final MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
bar.animate().alpha(0.0f);
viewPager.setVisibility(View.VISIBLE);
layoutParams.height = old_y;
viewPager.setLayoutParams(layoutParams);
break;
case MotionEvent.ACTION_UP:
Animation a = new Animation() {
#Override
protected void applyTransformation(float factor, Transformation t) {
float factorX = (float)scaleX + factor;
int currentPos = viewPager.getCurrentItem();
if (currentPos != 0) {
adapter.getItem(currentPos - 1).setScaleX(Math.min(factorX, 1));
adapter.getItem(currentPos - 1).setScaleY(Math.min(factorX, 1));
}
adapter.getItem(currentPos).setScaleX(Math.min(factorX, 1));
adapter.getItem(currentPos).setScaleY(Math.min(factorX, 1));
adapter.getItem(currentPos + 1).setScaleX(Math.min(factorX, 1));
adapter.getItem(currentPos + 1).setScaleY(Math.min(factorX, 1));
if(adapter.getItem(currentPos).getScaleX() > 0.1) {
if(factor == 1){
viewPager.setVisibility(View.GONE);
}
}
}
};
bar.animate().alpha(1.0f);
viewPager.setVisibility(View.GONE);
a.setDuration(zoomInDuration);
viewPager.startAnimation(a);
break;
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/navigationLayout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:animateLayoutChanges="false"
android:orientation="vertical">
<com.thanoscorp.uigestures.ViewPager.ViewPager
android:id="#+id/appViewpager"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:visibility="gone" />
<LinearLayout
android:id="#+id/barLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical">
<ImageView
android:id="#+id/overlayBar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="top"
android:paddingLeft="100dp"
android:paddingRight="100dp"
android:src="#mipmap/bar"
android:tint="#FF0000" />
</LinearLayout>
</FrameLayout>
I wasn't able to get rid of the animation, instead, i created another layout and added it to the window manager using wm.addView(vpLayout, lp); and removed it using wm.removeView(vpLayout);
and in case you are wondering what vpLayout is, I have seperated the ViewPager into another layout file and inflated it into a FrameLayout whose name is vpLayout. It took a while and it was worth it, but it gave birth to a new problem. Hope I will solve it too. Thanks :)
I have a custom view that extends SurfaceView overlaying the rest of my interface, it works on emulators and when the debugger is connected on my phone, but but when the phone is running on battery the view never clears.
public class CustomView extends SurfaceView {
private final Paint paint;
private final SurfaceHolder holder;
private final Context context;
private float strokeWidth = 4;
private boolean canvasAlreadyLocked = false;
public CustomView(Context viewContext, AttributeSet attrs)
{
super(viewContext, attrs);
Log.i("CustomView", "CustomView create context & attrs");
holder = getHolder();
context = viewContext;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(strokeWidth);
drawLine();
setZOrderOnTop(true);
holder.setFormat(PixelFormat.TRANSPARENT);
}
public void resume() {
Log.i("CustomView", "Resume the customview display.");
setZOrderOnTop(true);
holder.setFormat(PixelFormat.TRANSPARENT);
}
#Override
public void onAttachedToWindow(){
super.onAttachedToWindow();
setZOrderOnTop(true);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
protected void drawLine() {
if (!canvasAlreadyLocked) {
invalidate();
if (holder.getSurface().isValid()) {
try {
final Canvas canvas = holder.lockCanvas();
canvasAlreadyLocked = true;
if (canvas != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
paint.setColor(Color.BLACK);
paint.setStrokeWidth(strokeWidth * 2);
canvas.drawLine(0, getY(), getWidth(), getY(), paint);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(strokeWidth);
canvas.drawLine(0, getY(), getWidth(), getY(), paint);
holder.unlockCanvasAndPost(canvas);
canvasAlreadyLocked = false;
}
}
catch (IllegalArgumentException iae)
{
Log.w("CustomView", "Exception trying to lock canvas: "+iae.getMessage());
Log.getStackTraceString(iae);
}
}
}
}
private float getY() {
getHeight()/2;
}
}
I'm aware that some of the calls here are redundant - that is mostly the legacy of trying lots of different things to try to make it work. You will notice that I have already done everything recommended in this answer.
The layout works like this:
<FrameLayout 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.custom.CustomViewApp">
<FrameLayout
android:id="#+id/control"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true">
<com.custom.AutoFitTextureView
android:id="#+id/texture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
<ImageButton
android:id="#+id/gpsNotification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/gps_unfixed"
android:layout_gravity="right"
android:tint="#color/gps_unfixed"
android:background="#null" />
<ProgressBar
android:id="#+id/camera_spinner"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical"
android:gravity="center"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:visibility="invisible"
/>
<com.custom.CustomView
android:id="#+id/custom_view"
android:background="#color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
</FrameLayout>
</FrameLayout>
This is pulled in from a ViewFragment:
#Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);
gpsNotification = (ImageButton) view.findViewById(R.id.gpsNotification);
customView = (CustomView) view.findViewById(R.id.custom_view);
spinner = (ProgressBar) view.findViewById(R.id.camera_spinner);
spinner.setVisibility(VISIBLE);
}
I have tried to simplify this as far as I can and obviously there is a lot more happening in this scenario, but hopefully this is enough to indicate where the problem might be coming from.
The AutoFitTextureView is displaying the view from the camera.
When I run it in an emulator, everything displays as expected,
regardless of the battery settings.
When I run it on my phone connected by USB everything displays as expected.
When I run it on my phone disconnected, the view will usually, but not always, show as plain black - the AutoFitTextureView is completely obscured, but the line on the CustomView is drawn. The other components are visible, which leads me to suspect there is a problem with when or how I call setZOrderOnTop().
If I hit a breakpoint in the fragment and set the visibility of the CustomView to INVISIBLE I can immediately see everything else, but as you might expect I lose the overlay. Manually calling setZOrderOnTop at that point doesn't seem to change anything.
When it is running correctly, I can examine the structure with the Layout Inspector tool, but when the SurfaceView is opaque the Layout Inspector raises an Unexpected Error: Empty View Hierarchy message. I haven't been able to locate a corresponding error message in Logcat.
How do I ensure my SurfaceView-derived View is always transparent? If I can't guarantee that, is there any way I can test whether it currently is transparent so that I can intercede if it is not?
I think I have now solved this and the problem was that I was trying to put a SurfaceView derived type over a TextureView derived type. Switching out the AutofitTextureView for another SurfaceView showing the camera preview seems to have done the trick.
Is there any way to "forward" scroll events from one scrolling view to my bottom sheet, so that my bottom sheet begins to expand when I over-scroll the first scrolling view?
Consider this tiny app:
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
int peekHeight = getResources().getDimensionPixelSize(R.dimen.bottom_sheet_peek_height); // 96dp
View bottomSheet = findViewById(R.id.bottomSheet);
BottomSheetBehavior<View> behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setPeekHeight(peekHeight);
}
}
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- LinearLayout holding children to scroll through -->
</android.support.v4.widget.NestedScrollView>
<View
android:id="#+id/bottomSheet"
android:layout_width="300dp"
android:layout_height="400dp"
android:layout_gravity="center_horizontal"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"/>
</android.support.design.widget.CoordinatorLayout>
Out of the box, this works just fine. I see 96dp worth of my bottom sheet, and I can swipe it up and down as normal. Additionally, I can see my scrolling content, and I can scroll it up and down as normal.
Let's assume I'm at the state shown in the second image. My NestedScrollView is scrolled all the way to the bottom and my bottom sheet is collapsed. I'd like to be able to swipe upwards on the NestedScrollView (not on the bottom sheet) and, since it can't scroll any farther, have that swipe gesture instead be sent to the bottom sheet, so that it begins to expand. Basically, have the app behave as though my gesture had been performed on the bottom sheet, not the scroll view.
My first thought was to look at NestedScrollView.OnScrollChangeListener, but I couldn't get that to work since it stops being triggered at the boundaries of the scrolling content (after all, it listens for scroll changes, and nothing's changing when you're at the edges).
I also took a look at creating my own subclass of BottomSheetBehavior and trying to override onInterceptTouchEvent(), but ran into trouble in two places. First, I only want to capture events when the sibling scroll view is at the bottom, and I could do that, but I was now capturing all events (making it impossible to scroll the sibling back up). Second, the private field mIgnoreEvents inside BottomSheetBehavior was blocking the bottom sheet from actually expanding. I can use reflection to access this field and prevent it from blocking me, but that feels evil.
Edit: I spent some more time looking into AppBarLayout.ScrollingViewBehavior, since that seemed to be pretty close to what I wanted (it converts swipes on one view into resizing on another), but that appears to manually set the offset pixel by pixel, and bottom sheets don't quite behave that way.
This is an update with a more general solution. It now handles being hidden and "skip collapsed" of the standard bottom view behavior.
The following solution uses a custom BottomSheetBehavior. Here is a quick video of a small app based upon your posted app with the custom behavior in place:
MyBottomSheetBehavior extends BottomSheetBehavior and does the heavy lifting for the desired behavior. MyBottomSheetBehavior is passive until the NestedScrollView reaches its bottom scroll limit. onNestedScroll() identifies that the limit has been reached and offsets the bottom sheet by the amount of the scroll until the offset for the fully expanded bottom sheet is reached. This is the expansion logic.
Once the bottom sheet is released from the bottom, the bottom sheet is considered "captured" until the user lifts a finger from the screen. While the bottom sheet is captured, onNestPreScroll() handles moving the bottom sheet toward the bottom of the screen. This is the collapsing logic.
BottomSheetBehavior doesn't provide a means to manipulate the bottom sheet other than to completely collapse or expand it. Other functionality that is needed is locked up in package-private functions of the base behavior. To get around this, I created a new class called BottomSheetBehaviorAccessors that shares a package (android.support.design.widget) with the stock behavior. This class provides access to some package-private methods that are used in the new behavior.
MyBottomSheetBehavior also accommodates the callbacks of BottomSheetBehavior.BottomSheetCallback and other general functionality.
MyBottomSheetBehavior.java
public class MyBottomSheetBehavior<V extends View> extends BottomSheetBehaviorAccessors<V> {
// The bottom sheet that interests us.
private View mBottomSheet;
// Offset when sheet is expanded.
private int mMinOffset;
// Offset when sheet is collapsed.
private int mMaxOffset;
// This is the bottom of the bottom sheet's parent.
private int mParentBottom;
// True if the bottom sheet is being moved through nested scrolls from NestedScrollView.
private boolean mSheetCaptured = false;
// True if the bottom sheet is touched directly and being dragged.
private boolean mIsheetTouched = false;
// Set to true on ACTION_DOWN on the NestedScrollView
private boolean mScrollStarted = false;
#SuppressWarnings("unused")
public MyBottomSheetBehavior() {
}
#SuppressWarnings("unused")
public MyBottomSheetBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mSheetCaptured = false;
mIsheetTouched = parent.isPointInChildBounds(child, (int) ev.getX(), (int) ev.getY());
mScrollStarted = !mIsheetTouched;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
#Override
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
mMinOffset = Math.max(0, parent.getHeight() - child.getHeight());
mMaxOffset = Math.max(parent.getHeight() - getPeekHeight(), mMinOffset);
mBottomSheet = child;
mParentBottom = parent.getBottom();
return super.onLayoutChild(parent, child, layoutDirection);
}
#Override
public void onNestedPreScroll(#NonNull CoordinatorLayout coordinatorLayout,
#NonNull V child, #NonNull View target, int dx, int dy,
#NonNull int[] consumed, int type) {
if (dy >= 0 || !mSheetCaptured || type != ViewCompat.TYPE_TOUCH
|| !(target instanceof NestedScrollView)) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
return;
}
// Pointer moving downward (dy < 0: scrolling toward top of data)
if (child.getTop() - dy <= mMaxOffset) {
// Dragging...
ViewCompat.offsetTopAndBottom(child, -dy);
setStateInternalAccessor(STATE_DRAGGING);
consumed[1] = dy;
} else if (isHideable()) {
// Hide...
ViewCompat.offsetTopAndBottom(child, Math.min(-dy, mParentBottom - child.getTop()));
consumed[1] = dy;
} else if (mMaxOffset - child.getTop() > 0) {
// Collapsed...
ViewCompat.offsetTopAndBottom(child, mMaxOffset - child.getTop());
consumed[1] = dy;
}
if (consumed[1] != 0) {
dispatchOnSlideAccessor(child.getTop());
}
}
#Override
public void onNestedScroll(#NonNull CoordinatorLayout coordinatorLayout, #NonNull V child,
#NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
if (dyUnconsumed <= 0 || !(target instanceof NestedScrollView)
|| type != ViewCompat.TYPE_TOUCH || getState() == STATE_HIDDEN) {
mSheetCaptured = false;
} else if (!mSheetCaptured) {
// Capture the bottom sheet only if it is at its collapsed height.
mSheetCaptured = isSheetCollapsed();
}
if (!mSheetCaptured) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
return;
}
/*
If the pointer is moving upward (dyUnconsumed > 0) and the scroll view isn't
consuming scroll (dyConsumed == 0) then the scroll view must be at the end
of its scroll.
*/
if (child.getTop() - dyUnconsumed < mMinOffset) {
// Expanded...
ViewCompat.offsetTopAndBottom(child, mMinOffset - child.getTop());
} else {
// Dragging...
ViewCompat.offsetTopAndBottom(child, -dyUnconsumed);
setStateInternalAccessor(STATE_DRAGGING);
}
dispatchOnSlideAccessor(child.getTop());
}
#Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
if (mScrollStarted) {
// Ignore initial call to this method before anything has happened.
mScrollStarted = false;
} else if (!mIsheetTouched) {
snapBottomSheet();
}
super.onStopNestedScroll(coordinatorLayout, child, target);
}
private void snapBottomSheet() {
if ((mMaxOffset - mBottomSheet.getTop()) > (mMaxOffset - mMinOffset) / 2) {
setState(BottomSheetBehavior.STATE_EXPANDED);
} else if (shouldHideAccessor(mBottomSheet, 0)) {
setState(BottomSheetBehavior.STATE_HIDDEN);
} else {
setState(BottomSheetBehavior.STATE_COLLAPSED);
}
}
private boolean isSheetCollapsed() {
return mBottomSheet.getTop() == mMaxOffset;
}
#SuppressWarnings("unused")
private static final String TAG = "MyBottomSheetBehavior";
}
BottomSheetBehaviorAccessors
package android.support.design.widget; // important!
// A "friend" class to provide access to some package-private methods in `BottomSheetBehavior`.
public class BottomSheetBehaviorAccessors<V extends View> extends BottomSheetBehavior<V> {
#SuppressWarnings("unused")
protected BottomSheetBehaviorAccessors() {
}
#SuppressWarnings("unused")
public BottomSheetBehaviorAccessors(Context context, AttributeSet attrs) {
super(context, attrs);
}
protected void setStateInternalAccessor(int state) {
super.setStateInternal(state);
}
protected void dispatchOnSlideAccessor(int top) {
super.dispatchOnSlide(top);
}
protected boolean shouldHideAccessor(View child, float yvel) {
return mHideable && super.shouldHide(child, yvel);
}
#SuppressWarnings("unused")
private static final String TAG = "BehaviorAccessor";
}
MainActivity.java
public class MainActivity extends AppCompatActivity{
private View mBottomSheet;
MyBottomSheetBehavior<View> mBehavior;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayShowTitleEnabled(false);
int peekHeight = getResources().getDimensionPixelSize(R.dimen.bottom_sheet_peek_height); // 96dp
mBottomSheet = findViewById(R.id.bottomSheet);
mBehavior = (MyBottomSheetBehavior) MyBottomSheetBehavior.from(mBottomSheet);
mBehavior.setPeekHeight(peekHeight);
}
}
activity_main.xml
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="#+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:stateListAnimator="#null"
android:theme="#style/AppTheme.AppBarOverlay"
app:expanded="false"
app:layout_behavior="android.support.design.widget.AppBarLayout$Behavior">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/collapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:statusBarScrim="?attr/colorPrimaryDark">
<ImageView
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_marginTop="?attr/actionBarSize"
android:scaleType="centerCrop"
android:src="#drawable/seascape1"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="1.0"
tools:ignore="ContentDescription" />
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<com.example.bottomsheetoverscroll.MyNestedScrollView
android:id="#+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#android:color/holo_blue_light" />
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#android:color/holo_red_light" />
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#android:color/holo_blue_light" />
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#android:color/holo_red_light" />
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#android:color/holo_blue_light" />
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#android:color/holo_red_light" />
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#android:color/holo_green_light" />
</LinearLayout>
</com.example.bottomsheetoverscroll.MyNestedScrollView>
<TextView
android:id="#+id/bottomSheet"
android:layout_width="300dp"
android:layout_height="400dp"
android:layout_gravity="center_horizontal"
android:background="#android:color/white"
android:text="Bottom Sheet"
android:textAlignment="center"
android:textSize="24sp"
android:textStyle="bold"
app:layout_behavior="com.example.bottomsheetoverscroll.MyBottomSheetBehavior" />
<!--app:layout_behavior="android.support.design.widget.BottomSheetBehavior" />-->
</android.support.design.widget.CoordinatorLayout>
I have an custom view that is extended from MultiAutoCompleteTextView to create chiped view like contacts in gmail. when i a contact to this view and the keyborad is dismissed it casuse stack overflow. it happens only in my nexus 4 this is the logcat.
java.lang.StackOverflowError
at android.text.DynamicLayout.reflow(DynamicLayout.java:284)
at android.text.DynamicLayout.<init>(DynamicLayout.java:170)
at android.widget.TextView.makeSingleLayout(TextView.java:6134)
at android.widget.TextView.makeNewLayout(TextView.java:6032)
at android.widget.TextView.checkForRelayout(TextView.java:6571)
at android.widget.TextView.onRtlPropertiesChanged(TextView.java:8672)
at android.view.View.resolvePadding(View.java:12407)
at android.view.View.getPaddingLeft(View.java:15603)
at com.tokenautocomplete.TokenCompleteTextView.maxTextWidth(TokenCompleteTextView.java:260)
at com.tokenautocomplete.TokenCompleteTextView.access$1000(TokenCompleteTextView.java:54)
at com.tokenautocomplete.TokenCompleteTextView$ViewSpan.prepView(TokenCompleteTextView.java:822)
at com.tokenautocomplete.TokenCompleteTextView$ViewSpan.getSize(TokenCompleteTextView.java:841)
at com.tokenautocomplete.TokenCompleteTextView$TokenImageSpan.getSize(TokenCompleteTextView.java:885)
this is my prep view code
private void prepView() {
int widthSpec = MeasureSpec.makeMeasureSpec((int)maxTextWidth(), MeasureSpec.AT_MOST);
int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
view.measure(widthSpec, heightSpec);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
this is line 260 of tokenCompleteTextView
private float maxTextWidth() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
is use TokenAutoComplete libray for the token view.
this is my layout
<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#color/gray"
android:clickable="true">
<LinearLayout
android:id="#+id/llsearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="horizontal"
android:weightSum="4"
android:background="#color/listview_color"
android:layout_marginTop="#dimen/hdpi_4dp"
android:layout_marginBottom="#dimen/hdpi_4dp"
android:gravity="center_vertical">
<in.ispg.chipview.ConatctCompleteTextView
android:id="#+id/edtsearch"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="#string/search"
android:layout_weight="1"
android:textSize="#dimen/textsize_edittext"
android:textColor="#color/black"
android:paddingLeft="#dimen/hdpi_4dp"
android:paddingRight="#dimen/hdpi_4dp"
android:layout_marginLeft="#dimen/hdpi_8dp"
android:layout_marginRight="#dimen/hdpi_8dp"
android:singleLine="false"
android:minLines="1"
android:maxLines="5"
>
<requestFocus />
</in.ispg.chipview.ConatctCompleteTextView>
<Button
android:id="#+id/btnsearch"
android:layout_width="fill_parent"
android:layout_height="#dimen/hdpi_33dp"
android:text="#string/done"
android:layout_weight="3"
android:background="#drawable/send_button"
android:layout_marginRight="#dimen/hdpi_8dp"
android:textColor="#color/white"
android:gravity="center"
/>
<in.ispg.utils.FontTextView
android:id="#android:id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:textSize="#dimen/textsize_edittext"
android:textColor="#595959"
android:textStyle="bold"
android:text="" />
<ProgressBar
android:id="#+id/progressBar1"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:indeterminate="true"
android:indeterminateDrawable ="#drawable/progress"
android:visibility="gone" />
Note
I know how to dismiss the keyboard. that is not my problem. I get a stackoverflow error when i do so in a specific view.
I use this code to hide soft keyboard
// To close the soft keyboard
InputMethodManager inputMethod = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethod.hideSoftInputFromWindow(getView().getWindowToken(), 0);
1.) try this , hope it will solve your issue, i use the below code to HideSoftkeyboard
public void hideSoftKeyboard(Activity activity) {
InputMethodManager inputMethodManager = (InputMethodManager) activity
.getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(activity.getCurrentFocus()
.getWindowToken(), 0);
}
OR
If I need to care about when the keyboard appears and disappears (which is quite often) then what I do is customize my top-level layout class into one which overrides onMeasure(). The basic logic is that if the layout finds itself filling significantly less than the total area of the window, then a soft keyboard is probably showing.
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.widget.LinearLayout;
/*
* LinearLayoutThatDetectsSoftKeyboard - a variant of LinearLayout that can detect when
* the soft keyboard is shown and hidden (something Android can't tell you, weirdly).
*/
public class LinearLayoutThatDetectsSoftKeyboard extends LinearLayout {
public LinearLayoutThatDetectsSoftKeyboard(Context context, AttributeSet attrs) {
super(context, attrs);
}
public interface Listener {
public void onSoftKeyboardShown(boolean isShowing);
}
private Listener listener;
public void setListener(Listener listener) {
this.listener = listener;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = MeasureSpec.getSize(heightMeasureSpec);
Activity activity = (Activity)getContext();
Rect rect = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rect.top;
int screenHeight = activity.getWindowManager().getDefaultDisplay().getHeight();
int diff = (screenHeight - statusBarHeight) - height;
if (listener != null) {
listener.onSoftKeyboardShown(diff>128); // assume all soft keyboards are at least 128 pixels high
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
Then in your Activity class...
public class MyActivity extends Activity implements LinearLayoutThatDetectsSoftKeyboard.Listener {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
LinearLayoutThatDetectsSoftKeyboard mainLayout = (LinearLayoutThatDetectsSoftKeyboard)findViewById(R.id.main);
mainLayout.setListener(this);
...
}
#Override
public void onSoftKeyboardShown(boolean isShowing) {
// do whatever you need to do here
}
...
}
I had the same problem. I don't know why it happens. I tested in differents devices and in a emulator. This error happens just in some devices and in emulator I can't simulate it. This is what I did:
#Override
public int getSize(Paint paint, CharSequence charSequence, int i, int i2, Paint.FontMetricsInt fm) {
try{
prepView();
}catch(StackOverflowError error){
}
...
This code prevent the application close.