Related
I am investigating Android ViewPager animation in my current project.
I would like to give my users an affordance in relation to my ViewPager.
The effect I am looking for it to "shake" the ViewPager from left to right to partially show the neighbouring pages to the selected page.
When the user selects the first page (F), I would "Shake" between F and F + 1.
When the user selects the last page (L), I would "Shake" between L and L - 1.
Otherwise, when the user selects any other page (X), I would "Shake" between X - 1, X and X + 1.
I have tried the following approaches which have all given unsatisfactory results.
Fake drag using screen density to calculate shake distance and valueAnimator and ObjectAnimator.
Animating ViewPager.PageTransformer.
I am unable to get a satisfactory "Shake" effect.
I would like to achieve a Spring Dampening style of animation.
The FakeDrag was closest, although the effect was not reliable, it seemed to never work correctly the first time I started the animation.
Even the FakeDrag would not give the desired smooth shake between page swipes.
I would like to employ the physics-based animations on my view pager but couldn't see how.
Is it possible to achieve my desired animation?
UPDATE
I have developed this code which gives the desired effect, however its JANKY!
#Override
protected void onResume() {
super.onResume();
final Handler handler = new Handler();
handler.postDelayed(() -> animateViewPager(ANIMATION_OFFSET, ANIMATION_DURATION), ANIMATION_DELAY);
}
/**
* #param offset
* #param duration
*/
private void animateViewPager(final int offset, final int duration) {
if (animator.isRunning()) {
return;
}
animator.removeAllUpdateListeners();
animator.removeAllListeners();
animator.setIntValues(0, -offset);
animator.setDuration(duration);
animator.setRepeatCount(getRepeatCount());
animator.setRepeatMode(ValueAnimator.RESTART);
animator.addUpdateListener(constructUpdateListener());
animator.addListener(constructAnimatorListener());
animator.start();
}
/**
*
* #return
*/
private Animator.AnimatorListener constructAnimatorListener() {
return new AnimatorListenerAdapter() {
#Override
public void onAnimationStart(final Animator animation) {
animFactor = 1;
}
#Override
public void onAnimationEnd(final Animator animation) {
viewPager.endFakeDrag();
if (xAnimation == null) {
xAnimation = createSpringAnimation(viewPager, SpringAnimation.X, 0, SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
} else {
xAnimation.cancel();
}
viewPager.animate().x(viewPager.getWidth() * 0.1f).setDuration(0).start();
xAnimation.start();
}
#Override
public void onAnimationRepeat(final Animator animation) {
animFactor = -1;
}
};
}
/**
* #return
*/
private int getRepeatCount() {
if (isOnlyPage()) {
return 0;
}
if (isFirstListItem()) {
return REPEAT_ONCE;
}
if (isLastListItem()) {
return REPEAT_ONCE;
}
return REPEAT_TWICE;
}
#SuppressWarnings("StatementWithEmptyBody")
private ValueAnimator.AnimatorUpdateListener constructUpdateListener() {
return animation -> {
final Integer value = animFactor * (Integer) animation.getAnimatedValue();
if (viewPager.isFakeDragging()) {
} else {
viewPager.beginFakeDrag();
}
viewPager.fakeDragBy(value);
};
}
/**
* #param view
* #param property
* #param finalPosition
* #param stiffness
* #param dampingRatio
* #return
*/
private SpringAnimation createSpringAnimation(final View view, final DynamicAnimation.ViewProperty property, final float finalPosition, final float stiffness, final float dampingRatio) {
final SpringAnimation animation = new SpringAnimation(view, property);
final SpringForce springForce = new SpringForce(finalPosition);
springForce.setStiffness(stiffness);
springForce.setDampingRatio(dampingRatio);
animation.setSpring(springForce);
return animation;
}
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 6 years ago.
Improve this question
Some home launchers, like Samsung, Sony, HTC, Nova Launcher etc., support badge counters. How can I implement badge counters for all devices that support it?
1st of all badge view is not any launcher or device specific .If you want to implement Badge View in Android 1st you have to create custom class for badge view
public class BadgeView extends TextView {
public static final int POSITION_TOP_LEFT = 1;
public static final int POSITION_TOP_RIGHT = 2;
public static final int POSITION_BOTTOM_LEFT = 3;
public static final int POSITION_BOTTOM_RIGHT = 4;
public static final int POSITION_CENTER = 5;
private static final int DEFAULT_MARGIN_DIP = 5;
private static final int DEFAULT_LR_PADDING_DIP = 5;
private static final int DEFAULT_CORNER_RADIUS_DIP = 8;
private static final int DEFAULT_POSITION = POSITION_TOP_RIGHT;
private static final int DEFAULT_BADGE_COLOR = Color
.parseColor("#CCFF0000"); // Color.RED;
private static final int DEFAULT_TEXT_COLOR = Color.WHITE;
private static Animation fadeIn;
private static Animation fadeOut;
private Context context;
private View target;
private int badgePosition;
private int badgeMarginH;
private int badgeMarginV;
private int badgeColor;
private boolean isShown;
private ShapeDrawable badgeBg;
private int targetTabIndex;
public BadgeView(Context context) {
this(context, (AttributeSet) null, android.R.attr.textViewStyle);
}
public BadgeView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
/**
* Constructor -
*
* create a new BadgeView instance attached to a target
* {#link android.view.View}.
*
* #param context
* context for this view.
* #param target
* the View to attach the badge to.
*/
public BadgeView(Context context, View target) {
this(context, null, android.R.attr.textViewStyle, target, 0);
}
/**
* Constructor -
*
* create a new BadgeView instance attached to a target
* {#link android.widget.TabWidget} tab at a given index.
*
* #param context
* context for this view.
* #param target
* the TabWidget to attach the badge to.
* #param index
* the position of the tab within the target.
*/
public BadgeView(Context context, TabWidget target, int index) {
this(context, null, android.R.attr.textViewStyle, target, index);
}
public BadgeView(Context context, AttributeSet attrs, int defStyle) {
this(context, attrs, defStyle, null, 0);
}
public BadgeView(Context context, AttributeSet attrs, int defStyle,
View target, int tabIndex) {
super(context, attrs, defStyle);
init(context, target, tabIndex);
}
public BadgeView(Context context, SlidingTabStrip target,
int i) {
// TODO Auto-generated constructor stub
this(context, null, android.R.attr.textViewStyle, target, i);
}
private void init(Context context, View target, int tabIndex) {
this.context = context;
this.target = target;
this.targetTabIndex = tabIndex;
// apply defaults
badgePosition = DEFAULT_POSITION;
badgeMarginH = dipToPixels(DEFAULT_MARGIN_DIP);
badgeMarginV = badgeMarginH;
badgeColor = DEFAULT_BADGE_COLOR;
setTypeface(Typeface.DEFAULT_BOLD);
int paddingPixels = dipToPixels(DEFAULT_LR_PADDING_DIP);
setPadding(paddingPixels, 0, paddingPixels, 0);
setTextColor(DEFAULT_TEXT_COLOR);
fadeIn = new AlphaAnimation(0, 1);
fadeIn.setInterpolator(new DecelerateInterpolator());
fadeIn.setDuration(200);
fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
fadeOut.setDuration(200);
isShown = false;
if (this.target != null) {
applyTo(this.target);
} else {
show();
}
}
private void applyTo(View target) {
ViewGroup.LayoutParams lp = target.getLayoutParams();
ViewParent parent = target.getParent();
FrameLayout container = new FrameLayout(context);
if (target instanceof TabWidget) {
// set target to the relevant tab child container
target = ((TabWidget) target).getChildTabViewAt(targetTabIndex);
this.target = target;
((ViewGroup) target).addView(container, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
this.setVisibility(View.GONE);
container.addView(this);
} else if (target instanceof SlidingTabStrip) {
// set target to the relevant tab child container
View textView = ((SlidingTabStrip) target)
.getChildAt(targetTabIndex);
ViewGroup group = (ViewGroup) target;
// getting index of TexTView from SlidingTabStrip
int index = group.indexOfChild(textView);
Log.e("index", "" + index);
group.removeView(textView);
group.addView(container, index, lp);
container.addView(textView);
this.setVisibility(View.GONE);
container.addView(this);
group.invalidate();
} else {
// TODO verify that parent is indeed a ViewGroup
ViewGroup group = (ViewGroup) parent;
int index = group.indexOfChild(target);
group.removeView(target);
group.addView(container, index, lp);
container.addView(target);
this.setVisibility(View.GONE);
container.addView(this);
group.invalidate();
}
}
/**
* Make the badge visible in the UI.
*
*/
public void show() {
show(false, null);
}
/**
* Make the badge visible in the UI.
*
* #param animate
* flag to apply the default fade-in animation.
*/
public void show(boolean animate) {
show(animate, fadeIn);
}
/**
* Make the badge visible in the UI.
*
* #param anim
* Animation to apply to the view when made visible.
*/
public void show(Animation anim) {
show(true, anim);
}
/**
* Make the badge non-visible in the UI.
*
*/
public void hide() {
hide(false, null);
}
/**
* Make the badge non-visible in the UI.
*
* #param animate
* flag to apply the default fade-out animation.
*/
public void hide(boolean animate) {
hide(animate, fadeOut);
}
/**
* Make the badge non-visible in the UI.
*
* #param anim
* Animation to apply to the view when made non-visible.
*/
public void hide(Animation anim) {
hide(true, anim);
}
/**
* Toggle the badge visibility in the UI.
*
*/
public void toggle() {
toggle(false, null, null);
}
/**
* Toggle the badge visibility in the UI.
*
* #param animate
* flag to apply the default fade-in/out animation.
*/
public void toggle(boolean animate) {
toggle(animate, fadeIn, fadeOut);
}
/**
* Toggle the badge visibility in the UI.
*
* #param animIn
* Animation to apply to the view when made visible.
* #param animOut
* Animation to apply to the view when made non-visible.
*/
public void toggle(Animation animIn, Animation animOut) {
toggle(true, animIn, animOut);
}
private void show(boolean animate, Animation anim) {
if (getBackground() == null) {
if (badgeBg == null) {
badgeBg = getDefaultBackground();
}
setBackgroundDrawable(badgeBg);
}
applyLayoutParams();
if (animate) {
this.startAnimation(anim);
}
this.setVisibility(View.VISIBLE);
isShown = true;
}
private void hide(boolean animate, Animation anim) {
this.setVisibility(View.GONE);
if (animate) {
this.startAnimation(anim);
}
isShown = false;
}
private void toggle(boolean animate, Animation animIn, Animation animOut) {
if (isShown) {
hide(animate && (animOut != null), animOut);
} else {
show(animate && (animIn != null), animIn);
}
}
/**
* Increment the numeric badge label. If the current badge label cannot be
* converted to an integer value, its label will be set to "0".
*
* #param offset
* the increment offset.
*/
public int increment(int offset) {
CharSequence txt = getText();
int i;
if (txt != null) {
try {
i = Integer.parseInt(txt.toString());
} catch (NumberFormatException e) {
i = 0;
}
} else {
i = 0;
}
i = i + offset;
setText(String.valueOf(i));
return i;
}
/**
* Decrement the numeric badge label. If the current badge label cannot be
* converted to an integer value, its label will be set to "0".
*
* #param offset
* the decrement offset.
*/
public int decrement(int offset) {
return increment(-offset);
}
private ShapeDrawable getDefaultBackground() {
int r = dipToPixels(DEFAULT_CORNER_RADIUS_DIP);
float[] outerR = new float[] { r, r, r, r, r, r, r, r };
RoundRectShape rr = new RoundRectShape(outerR, null, null);
ShapeDrawable drawable = new ShapeDrawable(rr);
drawable.getPaint().setColor(badgeColor);
return drawable;
}
private void applyLayoutParams() {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, AbsoluteLayout.LayoutParams.WRAP_CONTENT);
switch (badgePosition) {
case POSITION_TOP_LEFT:
lp.gravity = Gravity.LEFT | Gravity.TOP;
lp.setMargins(badgeMarginH, badgeMarginV, 0, 0);
break;
case POSITION_TOP_RIGHT:
lp.gravity = Gravity.RIGHT | Gravity.TOP;
lp.setMargins(0, badgeMarginV, badgeMarginH, 0);
break;
case POSITION_BOTTOM_LEFT:
lp.gravity = Gravity.LEFT | Gravity.BOTTOM;
lp.setMargins(badgeMarginH, 0, 0, badgeMarginV);
break;
case POSITION_BOTTOM_RIGHT:
lp.gravity = Gravity.RIGHT | Gravity.BOTTOM;
lp.setMargins(0, 0, badgeMarginH, badgeMarginV);
break;
case POSITION_CENTER:
lp.gravity = Gravity.CENTER;
lp.setMargins(0, 0, 0, 0);
break;
default:
break;
}
setLayoutParams(lp);
}
/**
* Returns the target View this badge has been attached to.
*
*/
public View getTarget() {
return target;
}
/**
* Is this badge currently visible in the UI?
*
*/
#Override
public boolean isShown() {
return isShown;
}
/**
* Returns the positioning of this badge.
*
* one of POSITION_TOP_LEFT, POSITION_TOP_RIGHT, POSITION_BOTTOM_LEFT,
* POSITION_BOTTOM_RIGHT, POSTION_CENTER.
*
*/
public int getBadgePosition() {
return badgePosition;
}
/**
* Set the positioning of this badge.
*
* #param layoutPosition
* one of POSITION_TOP_LEFT, POSITION_TOP_RIGHT,
* POSITION_BOTTOM_LEFT, POSITION_BOTTOM_RIGHT, POSTION_CENTER.
*
*/
public void setBadgePosition(int layoutPosition) {
this.badgePosition = layoutPosition;
}
/**
* Returns the horizontal margin from the target View that is applied to
* this badge.
*
*/
public int getHorizontalBadgeMargin() {
return badgeMarginH;
}
/**
* Returns the vertical margin from the target View that is applied to this
* badge.
*
*/
public int getVerticalBadgeMargin() {
return badgeMarginV;
}
/**
* Set the horizontal/vertical margin from the target View that is applied
* to this badge.
*
* #param badgeMargin
* the margin in pixels.
*/
public void setBadgeMargin(int badgeMargin) {
this.badgeMarginH = badgeMargin;
this.badgeMarginV = badgeMargin;
}
/**
* Set the horizontal/vertical margin from the target View that is applied
* to this badge.
*
* #param horizontal
* margin in pixels.
* #param vertical
* margin in pixels.
*/
public void setBadgeMargin(int horizontal, int vertical) {
this.badgeMarginH = horizontal;
this.badgeMarginV = vertical;
}
/**
* Returns the color value of the badge background.
*
*/
public int getBadgeBackgroundColor() {
return badgeColor;
}
/**
* Set the color value of the badge background.
*
* #param badgeColor
* the badge background color.
*/
public void setBadgeBackgroundColor(int badgeColor) {
this.badgeColor = badgeColor;
badgeBg = getDefaultBackground();
}
private int dipToPixels(int dip) {
Resources r = getResources();
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
r.getDisplayMetrics());
return (int) px;
}
}
And wherever you want badge view just initialize and use it
BadgeView badge;
This is the tutorial I started with.
http://www.androidhive.info/2014/12/android-uploading-camera-image-video-to-server-with-progress-bar/
Basically, I've uploaded an image and and I want to display it in a custom image viewer.
I've adjusted my php.ini like so to accept large uploads.
memory_limit = 128M
post_max_size = 1G
file_uploads = On
upload_max_filesize = 1G
max_file_uploads = 20
This is the error I'm getting from the logcat
03-15 01:06:36.843: W/OpenGLRenderer(14403): Bitmap too large to be uploaded into a texture (5312x2988, max=4096x4096)
OK, so the problem is, when I try to display a picture that's 6M, 8M, 9M or 12MEGAPIXELS everything is fine. When I try display a 16Megapixels photo, it doesn't display.
Ok, but the image viewer in the tutorial displays the image. But I'm using a different class to display the images. Image viewer seems to display an image from you SdCard. FeedImageView allows you to display a picture saved on a server.
public class FeedImageView extends ImageView {
public interface ResponseObserver {
public void onError();
public void onSuccess();
}
private ResponseObserver mObserver;
public void setResponseObserver(ResponseObserver observer) {
mObserver = observer;
}
/**
* The URL of the network image to load
*/
private String mUrl;
/**
* Resource ID of the image to be used as a placeholder until the network
* image is loaded.
*/
private int mDefaultImageId;
/**
* Resource ID of the image to be used if the network response fails.
*/
private int mErrorImageId;
/**
* Local copy of the ImageLoader.
*/
private ImageLoader mImageLoader;
/**
* Current ImageContainer. (either in-flight or finished)
*/
private ImageContainer mImageContainer;
public FeedImageView(Context context) {
this(context, null);
}
public FeedImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FeedImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Sets URL of the image that should be loaded into this view. Note that
* calling this will immediately either set the cached image (if available)
* or the default image specified by
* {#link VolleyImageView#setDefaultImageResId(int)} on the view.
*
* NOTE: If applicable, {#link VolleyImageView#setDefaultImageResId(int)}
* and {#link VolleyImageView#setErrorImageResId(int)} should be called
* prior to calling this function.
*
* #param url
* The URL that should be loaded into this ImageView.
* #param imageLoader
* ImageLoader that will be used to make the request.
*/
public void setImageUrl(String url, ImageLoader imageLoader) {
mUrl = url;
mImageLoader = imageLoader;
// The URL has potentially changed. See if we need to load it.
loadImageIfNecessary(false);
}
/**
* Sets the default image resource ID to be used for this view until the
* attempt to load it completes.
*/
public void setDefaultImageResId(int defaultImage) {
mDefaultImageId = defaultImage;
}
/**
* Sets the error image resource ID to be used for this view in the event
* that the image requested fails to load.
*/
public void setErrorImageResId(int errorImage) {
mErrorImageId = errorImage;
}
/**
* Loads the image for the view if it isn't already loaded.
*
* #param isInLayoutPass
* True if this was invoked from a layout pass, false otherwise.
*/
private void loadImageIfNecessary(final boolean isInLayoutPass) {
final int width = getWidth();
int height = getHeight();
boolean isFullyWrapContent = getLayoutParams() != null
&& getLayoutParams().height == LayoutParams.WRAP_CONTENT
&& getLayoutParams().width == LayoutParams.WRAP_CONTENT;
// if the view's bounds aren't known yet, and this is not a
// wrap-content/wrap-content
// view, hold off on loading the image.
if (width == 0 && height == 0 && !isFullyWrapContent) {
return;
}
// if the URL to be loaded in this view is empty, cancel any old
// requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
// if there was an old request in this view, check if it needs to be
// canceled.
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
// if the request is from the same URL, return.
return;
} else {
// if there is a pre-existing request, cancel it if it's
// fetching a different URL.
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}
// The pre-existing content of this view didn't match the current URL.
// Load the new image
// from the network.
ImageContainer newContainer = mImageLoader.get(mUrl,
new ImageListener() {
#Override
public void onErrorResponse(VolleyError error) {
if (mErrorImageId != 0) {
setImageResource(mErrorImageId);
}
if (mObserver != null) {
mObserver.onError();
}
}
#Override
public void onResponse(final ImageContainer response,
boolean isImmediate) {
// If this was an immediate response that was delivered
// inside of a layout
// pass do not set the image immediately as it will
// trigger a requestLayout
// inside of a layout. Instead, defer setting the image
// by posting back to
// the main thread.
if (isImmediate && isInLayoutPass) {
post(new Runnable() {
#Override
public void run() {
onResponse(response, false);
}
});
return;
}
int bWidth = 0, bHeight = 0;
if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
bWidth = response.getBitmap().getWidth();
bHeight = response.getBitmap().getHeight();
adjustImageAspect(bWidth, bHeight);
} else if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
}
if (mObserver != null) {
mObserver.onSuccess();
}
}
});
// update the ImageContainer to be the new bitmap container.
mImageContainer = newContainer;
}
private void setDefaultImageOrNull() {
if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
} else {
setImageBitmap(null);
}
}
#Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
loadImageIfNecessary(true);
}
#Override
protected void onDetachedFromWindow() {
if (mImageContainer != null) {
// If the view was bound to an image request, cancel it and clear
// out the image from the view.
mImageContainer.cancelRequest();
setImageBitmap(null);
// also clear out the container so we can reload the image if
// necessary.
mImageContainer = null;
}
super.onDetachedFromWindow();
}
#Override
protected void drawableStateChanged() {
super.drawableStateChanged();
invalidate();
}
/*
* Adjusting imageview height
*/
private void adjustImageAspect(int bWidth, int bHeight) {
LinearLayout.LayoutParams params = (LayoutParams) getLayoutParams();
if (bWidth == 0 || bHeight == 0)
return;
int swidth = getWidth();
int new_height = 0;
new_height = swidth * bHeight / bWidth;
params.width = swidth;
params.height = new_height;
setLayoutParams(params);
}
}
How can I allow the picture to show with 16 megapixels in the FeedImageView class like the regular
private ImageView imgPreview;
class the tutorial uses?
Set
android:hardwareAccelerated="false"
&
android:largeHeap="true"
within the application tag of the manifest file.
It seems like this will work fine for now. The animations are a tad bit choppy as I scroll the the pictures with these parameters set.
<application
android:name="com.clxxxii.givenchy"
android:allowBackup="true"
android:hardwareAccelerated="false"
android:icon="#drawable/givenchy"
android:label="#string/app_name"
android:largeHeap="true"
android:theme="#style/CustomActionBarTheme" >
For displaying the image on image view, I prefer resizing the image and loading that image.
You can resize the image by referring the below mentioned link and this will also going to solve texture issue
http://developer.android.com/training/displaying-bitmaps/index.html
I'm working on a project which needs me to display some dynamic texts based on users' selection on a button.
I know how to do the text display part, but I was stuck on how I can display different text properly on a fixed size button.
For example: "Apple" and "I have an Apple". How can I achieve the result that when displaying "Apple", the text size will be bigger and fit the button, and when "I have an Apple" the text will be smaller and may become three lines?
Thank you!
Android 8.0 supports Autosizing TextViews so you just have to specify android:autoSizeTextType="uniform". For older versions, you can use android.support.v7.widget.AppCompatTextView with app:autoSizeTextType="uniform".
By chance, it also works for buttons and for older versions just use android.support.v7.widget.AppCompatButton instead.
Hope this helped.
Take a look at this question Auto Scale TextView Text to Fit within Bounds. The same technique should apply to a button.
(yes, it is much more complicated than it seems like it should be.)
Style.xml:
<style name="Widget.Button.CustomStyle" parent="Widget.MaterialComponents.Button">
<item name="android:minHeight">50dp</item>
<item name="android:maxWidth">300dp</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">16sp</item>
<item name="backgroundTint">#color/white</item>
<item name="cornerRadius">25dp</item>
<item name="autoSizeTextType">uniform</item>
<item name="autoSizeMinTextSize">10sp</item>
<item name="autoSizeMaxTextSize">16sp</item>
<item name="autoSizeStepGranularity">2sp</item>
<item name="android:maxLines">1</item>
<item name="android:textColor">#color/colorPrimary</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:lineSpacingExtra">4sp</item>
<item name="android:gravity">center</item>
</style>
Usage:
<com.google.android.material.button.MaterialButton
android:id="#+id/blah"
style="#style/Widget.Button.CustomStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Your long text, to the infinity and beyond!!! Why not :)" />
Result:
[Source: https://stackoverflow.com/a/59302886/421467 ]
I know this question is a few years old, but I want to add a full solution for future reference.
This code is based on AutoFitTextView and has been adapted for Buttons. Specifically it also considers the text width to avoid word-breaks when resizing.
For all licensing information visit the above link.
You'll need at least to java files:
AutoSizeTextButton.java
public class AutoSizeTextButton extends android.support.v7.widget.AppCompatButton implements AutofitHelper.OnTextSizeChangeListener{
private AutofitHelper mHelper;
public AutoSizeTextButton(Context context) {
this(context, null, 0);
}
public AutoSizeTextButton(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public AutoSizeTextButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr);
}
private void init(AttributeSet attrs, int defStyleAttr) {
mHelper = AutofitHelper.create(this, attrs, defStyleAttr)
.addOnTextSizeChangeListener(this);
}
// Getters and Setters
/**
* {#inheritDoc}
*/
#Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
if (mHelper != null) {
mHelper.setTextSize(unit, size);
}
}
/**
* {#inheritDoc}
*/
#Override
public void setLines(int lines) {
super.setLines(lines);
if (mHelper != null) {
mHelper.setMaxLines(lines);
}
}
/**
* {#inheritDoc}
*/
#Override
public void setMaxLines(int maxLines) {
super.setMaxLines(maxLines);
if (mHelper != null) {
mHelper.setMaxLines(maxLines);
}
}
/**
* Returns the {#link AutofitHelper} for this View.
*/
public AutofitHelper getAutofitHelper() {
return mHelper;
}
/**
* Returns whether or not the text will be automatically re-sized to fit its constraints.
*/
public boolean isSizeToFit() {
return mHelper.isEnabled();
}
/**
* Sets the property of this field (sizeToFit), to automatically resize the text to fit its
* constraints.
*/
public void setSizeToFit() {
setSizeToFit(true);
}
/**
* If true, the text will automatically be re-sized to fit its constraints; if false, it will
* act like a normal View.
*
* #param sizeToFit
*/
public void setSizeToFit(boolean sizeToFit) {
mHelper.setEnabled(sizeToFit);
}
/**
* Returns the maximum size (in pixels) of the text in this View.
*/
public float getMaxTextSize() {
return mHelper.getMaxTextSize();
}
/**
* Set the maximum text size to the given value, interpreted as "scaled pixel" units. This size
* is adjusted based on the current density and user font size preference.
*
* #param size The scaled pixel size.
*
* #attr ref android.R.styleable#TextView_textSize
*/
public void setMaxTextSize(float size) {
mHelper.setMaxTextSize(size);
}
/**
* Set the maximum text size to a given unit and value. See TypedValue for the possible
* dimension units.
*
* #param unit The desired dimension unit.
* #param size The desired size in the given units.
*
* #attr ref android.R.styleable#TextView_textSize
*/
public void setMaxTextSize(int unit, float size) {
mHelper.setMaxTextSize(unit, size);
}
/**
* Returns the minimum size (in pixels) of the text in this View.
*/
public float getMinTextSize() {
return mHelper.getMinTextSize();
}
/**
* Set the minimum text size to the given value, interpreted as "scaled pixel" units. This size
* is adjusted based on the current density and user font size preference.
*
* #param minSize The scaled pixel size.
*
* #attr ref R.styleable#AutofitButton_minTextSize
*/
public void setMinTextSize(int minSize) {
mHelper.setMinTextSize(TypedValue.COMPLEX_UNIT_SP, minSize);
}
/**
* Set the minimum text size to a given unit and value. See TypedValue for the possible
* dimension units.
*
* #param unit The desired dimension unit.
* #param minSize The desired size in the given units.
*
* #attr ref R.styleable#AutofitButton_minTextSize
*/
public void setMinTextSize(int unit, float minSize) {
mHelper.setMinTextSize(unit, minSize);
}
/**
* Returns the amount of precision used to calculate the correct text size to fit within its
* bounds.
*/
public float getPrecision() {
return mHelper.getPrecision();
}
/**
* Set the amount of precision used to calculate the correct text size to fit within its
* bounds. Lower precision is more precise and takes more time.
*
* #param precision The amount of precision.
*/
public void setPrecision(float precision) {
mHelper.setPrecision(precision);
}
#Override
public void onTextSizeChange(float textSize, float oldTextSize) {
// do nothing
}
}
AutofitHelper.java
/**
* A helper class to enable automatically resizing a {#link android.widget.Button}`s {#code textSize} to fit
* within its bounds.
*
* #attr ref R.styleable.AutofitButton_sizeToFit
* #attr ref R.styleable.AutofitButton_minTextSize
* #attr ref R.styleable.AutofitButton_precision
*/
public class AutofitHelper {
private static final String TAG = "AutoFitTextHelper";
private static final boolean SPEW = false;
// Minimum size of the text in pixels
private static final int DEFAULT_MIN_TEXT_SIZE = 8; //sp
// How precise we want to be when reaching the target textWidth size
private static final float DEFAULT_PRECISION = 0.5f;
/**
* Creates a new instance of {#code AutofitHelper} that wraps a {#link android.widget.Button} and enables
* automatically sizing the text to fit.
*/
public static AutofitHelper create(Button view) {
return create(view, null, 0);
}
/**
* Creates a new instance of {#code AutofitHelper} that wraps a {#link android.widget.Button} and enables
* automatically sizing the text to fit.
*/
public static AutofitHelper create(Button view, AttributeSet attrs) {
return create(view, attrs, 0);
}
/**
* Creates a new instance of {#code AutofitHelper} that wraps a {#link android.widget.Button} and enables
* automatically sizing the text to fit.
*/
public static AutofitHelper create(Button view, AttributeSet attrs, int defStyle) {
AutofitHelper helper = new AutofitHelper(view);
boolean sizeToFit = true;
if (attrs != null) {
Context context = view.getContext();
int minTextSize = (int) helper.getMinTextSize();
float precision = helper.getPrecision();
TypedArray ta = context.obtainStyledAttributes(
attrs,
R.styleable.AutofitButton,
defStyle,
0);
sizeToFit = ta.getBoolean(R.styleable.AutofitButton_sizeToFit, sizeToFit);
minTextSize = ta.getDimensionPixelSize(R.styleable.AutofitButton_minTextSize,
minTextSize);
precision = ta.getFloat(R.styleable.AutofitButton_precision, precision);
ta.recycle();
helper.setMinTextSize(TypedValue.COMPLEX_UNIT_PX, minTextSize)
.setPrecision(precision);
}
helper.setEnabled(sizeToFit);
return helper;
}
/**
* Re-sizes the textSize of the TextView so that the text fits within the bounds of the View.
*/
private static void autofit(Button view, TextPaint paint, float minTextSize, float maxTextSize,
int maxLines, float precision) {
if (maxLines <= 0 || maxLines == Integer.MAX_VALUE) {
// Don't auto-size since there's no limit on lines.
return;
}
int targetWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();
if (targetWidth <= 0) {
return;
}
CharSequence text = view.getText();
TransformationMethod method = view.getTransformationMethod();
if (method != null) {
text = method.getTransformation(text, view);
}
Context context = view.getContext();
Resources r = Resources.getSystem();
DisplayMetrics displayMetrics;
float size = maxTextSize;
float high = size;
float low = 0;
if (context != null) {
r = context.getResources();
}
displayMetrics = r.getDisplayMetrics();
paint.set(view.getPaint());
paint.setTextSize(size);
if ((maxLines == 1 && paint.measureText(text, 0, text.length()) > targetWidth)
|| getLineCount(text, paint, size, targetWidth, displayMetrics) > maxLines
|| getMaxWordWidth(text, paint, size, displayMetrics) > targetWidth) {
size = getAutofitTextSize(text, getMaxWord(text, paint), paint, targetWidth, maxLines, low, high, precision,
displayMetrics);
}
if (size < minTextSize) {
size = minTextSize;
}
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
}
/**
* Recursive binary search to find the best size for the text.
*/
private static float getAutofitTextSize(CharSequence text, String widestWord, TextPaint paint,
float targetWidth, int maxLines, float low, float high, float precision,
DisplayMetrics displayMetrics) {
float mid = (low + high) / 2.0f;
int lineCount = 1;
StaticLayout layout = null;
paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mid,
displayMetrics));
if (maxLines != 1) {
layout = new StaticLayout(text, paint, (int)targetWidth, Layout.Alignment.ALIGN_NORMAL,
1.0f, 0.0f, true);
lineCount = layout.getLineCount();
}
if (SPEW) Log.d(TAG, "low=" + low + " high=" + high + " mid=" + mid +
" target=" + targetWidth + " maxLines=" + maxLines + " lineCount=" + lineCount);
if (lineCount > maxLines) {
// For the case that `text` has more newline characters than `maxLines`.
if ((high - low) < precision) {
return low;
}
return getAutofitTextSize(text, widestWord, paint, targetWidth, maxLines, low, mid, precision,
displayMetrics);
}
else if (lineCount < maxLines) {
return getAutofitTextSize(text, widestWord, paint, targetWidth, maxLines, mid, high, precision,
displayMetrics);
}
else {
float maxLineWidth = 0;
if (maxLines == 1) {
maxLineWidth = paint.measureText(text, 0, text.length());
} else {
for (int i = 0; i < lineCount; i++) {
if (layout.getLineWidth(i) > maxLineWidth) {
maxLineWidth = layout.getLineWidth(i);
}
}
}
float maxWordWidth = paint.measureText(widestWord, 0, widestWord.length());
if(maxWordWidth > maxLineWidth){
maxLineWidth = maxWordWidth;
}
if ((high - low) < precision) {
return low;
} else if (maxLineWidth > targetWidth) {
return getAutofitTextSize(text, widestWord, paint, targetWidth, maxLines, low, mid, precision,
displayMetrics);
} else if (maxLineWidth < targetWidth) {
return getAutofitTextSize(text, widestWord, paint, targetWidth, maxLines, mid, high, precision,
displayMetrics);
} else {
return mid;
}
}
}
private static int getLineCount(CharSequence text, TextPaint paint, float size, float width,
DisplayMetrics displayMetrics) {
paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, size,
displayMetrics));
StaticLayout layout = new StaticLayout(text, paint, (int)width,
Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true);
return layout.getLineCount();
}
private static int getMaxLines(Button view) {
int maxLines = -1; // No limit (Integer.MAX_VALUE also means no limit)
TransformationMethod method = view.getTransformationMethod();
if (method != null && method instanceof SingleLineTransformationMethod) {
maxLines = 1;
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// setMaxLines() and getMaxLines() are only available on android-16+
maxLines = view.getMaxLines();
}
return maxLines;
}
private static float getMaxWordWidth(CharSequence text, TextPaint paint, float size,
DisplayMetrics displayMetrics) {
paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, size,
displayMetrics));
String maxWord = getMaxWord(text, paint);
return paint.measureText(maxWord, 0, maxWord.length());
}
private static String getMaxWord(CharSequence text, TextPaint paint) {
String textStr = text.toString();
textStr = textStr.replace("-", "- ");
String[] words = textStr.split("[ \u00AD\u200B]");
String maxWord = "";
float maxWordWidth = 0;
for (String word : words) {
float wordWidth = paint.measureText(word, 0, word.length());
if (wordWidth > maxWordWidth){
maxWordWidth = wordWidth;
maxWord = word;
}
}
return maxWord;
}
// Attributes
private Button mButton;
private TextPaint mPaint;
/**
* Original textSize of the TextView.
*/
private float mTextSize;
private int mMaxLines;
private float mMinTextSize;
private float mMaxTextSize;
private float mPrecision;
private boolean mEnabled;
private boolean mIsAutofitting;
private ArrayList<OnTextSizeChangeListener> mListeners;
private TextWatcher mTextWatcher = new AutofitTextWatcher();
private View.OnLayoutChangeListener mOnLayoutChangeListener =
new AutofitOnLayoutChangeListener();
private AutofitHelper(Button view) {
final Context context = view.getContext();
float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
mButton = view;
mPaint = new TextPaint();
setRawTextSize(view.getTextSize());
mMaxLines = getMaxLines(view);
mMinTextSize = scaledDensity * DEFAULT_MIN_TEXT_SIZE;
mMaxTextSize = mTextSize;
mPrecision = DEFAULT_PRECISION;
}
/**
* Adds an {#link OnTextSizeChangeListener} to the list of those whose methods are called
* whenever the {#link android.widget.Button}'s {#code textSize} changes.
*/
public AutofitHelper addOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<OnTextSizeChangeListener>();
}
mListeners.add(listener);
return this;
}
/**
* Removes the specified {#link OnTextSizeChangeListener} from the list of those whose methods
* are called whenever the {#link android.widget.Button}'s {#code textSize} changes.
*/
public AutofitHelper removeOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
if (mListeners != null) {
mListeners.remove(listener);
}
return this;
}
/**
* Returns the amount of precision used to calculate the correct text size to fit within its
* bounds.
*/
public float getPrecision() {
return mPrecision;
}
/**
* Set the amount of precision used to calculate the correct text size to fit within its
* bounds. Lower precision is more precise and takes more time.
*
* #param precision The amount of precision.
*/
public AutofitHelper setPrecision(float precision) {
if (mPrecision != precision) {
mPrecision = precision;
autofit();
}
return this;
}
/**
* Returns the minimum size (in pixels) of the text.
*/
public float getMinTextSize() {
return mMinTextSize;
}
/**
* Set the minimum text size to the given value, interpreted as "scaled pixel" units. This size
* is adjusted based on the current density and user font size preference.
*
* #param size The scaled pixel size.
*
* #attr ref me.grantland.R.styleable#AutofitTextView_minTextSize
*/
public AutofitHelper setMinTextSize(float size) {
return setMinTextSize(TypedValue.COMPLEX_UNIT_SP, size);
}
/**
* Set the minimum text size to a given unit and value. See TypedValue for the possible
* dimension units.
*
* #param unit The desired dimension unit.
* #param size The desired size in the given units.
*
* #attr ref me.grantland.R.styleable#AutofitTextView_minTextSize
*/
public AutofitHelper setMinTextSize(int unit, float size) {
Context context = mButton.getContext();
Resources r = Resources.getSystem();
if (context != null) {
r = context.getResources();
}
setRawMinTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()));
return this;
}
private void setRawMinTextSize(float size) {
if (size != mMinTextSize) {
mMinTextSize = size;
autofit();
}
}
/**
* Returns the maximum size (in pixels) of the text.
*/
public float getMaxTextSize() {
return mMaxTextSize;
}
/**
* Set the maximum text size to the given value, interpreted as "scaled pixel" units. This size
* is adjusted based on the current density and user font size preference.
*
* #param size The scaled pixel size.
*
* #attr ref android.R.styleable#TextView_textSize
*/
public AutofitHelper setMaxTextSize(float size) {
return setMaxTextSize(TypedValue.COMPLEX_UNIT_SP, size);
}
/**
* Set the maximum text size to a given unit and value. See TypedValue for the possible
* dimension units.
*
* #param unit The desired dimension unit.
* #param size The desired size in the given units.
*
* #attr ref android.R.styleable#TextView_textSize
*/
public AutofitHelper setMaxTextSize(int unit, float size) {
Context context = mButton.getContext();
Resources r = Resources.getSystem();
if (context != null) {
r = context.getResources();
}
setRawMaxTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()));
return this;
}
private void setRawMaxTextSize(float size) {
if (size != mMaxTextSize) {
mMaxTextSize = size;
autofit();
}
}
/**
* #see TextView#getMaxLines()
*/
public int getMaxLines() {
return mMaxLines;
}
/**
* #see TextView#setMaxLines(int)
*/
public AutofitHelper setMaxLines(int lines) {
if (mMaxLines != lines) {
mMaxLines = lines;
autofit();
}
return this;
}
/**
* Returns whether or not automatically resizing text is enabled.
*/
public boolean isEnabled() {
return mEnabled;
}
/**
* Set the enabled state of automatically resizing text.
*/
public AutofitHelper setEnabled(boolean enabled) {
if (mEnabled != enabled) {
mEnabled = enabled;
if (enabled) {
mButton.addTextChangedListener(mTextWatcher);
mButton.addOnLayoutChangeListener(mOnLayoutChangeListener);
autofit();
} else {
mButton.removeTextChangedListener(mTextWatcher);
mButton.removeOnLayoutChangeListener(mOnLayoutChangeListener);
mButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
}
}
return this;
}
/**
* Returns the original text size of the View.
*
* #see TextView#getTextSize()
*/
public float getTextSize() {
return mTextSize;
}
/**
* Set the original text size of the View.
*
* #see TextView#setTextSize(float)
*/
public void setTextSize(float size) {
setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
}
/**
* Set the original text size of the View.
*
* #see TextView#setTextSize(int, float)
*/
public void setTextSize(int unit, float size) {
if (mIsAutofitting) {
// We don't want to update the TextView's actual textSize while we're autofitting
// since it'd get set to the autofitTextSize
return;
}
Context context = mButton.getContext();
Resources r = Resources.getSystem();
if (context != null) {
r = context.getResources();
}
setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()));
}
private void setRawTextSize(float size) {
if (mTextSize != size) {
mTextSize = size;
}
}
private void autofit() {
float oldTextSize = mButton.getTextSize();
float textSize;
mIsAutofitting = true;
autofit(mButton, mPaint, mMinTextSize, mMaxTextSize, mMaxLines, mPrecision);
mIsAutofitting = false;
textSize = mButton.getTextSize();
if (textSize != oldTextSize) {
sendTextSizeChange(textSize, oldTextSize);
}
}
private void sendTextSizeChange(float textSize, float oldTextSize) {
if (mListeners == null) {
return;
}
for (OnTextSizeChangeListener listener : mListeners) {
listener.onTextSizeChange(textSize, oldTextSize);
}
}
private class AutofitTextWatcher implements TextWatcher {
#Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
// do nothing
}
#Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
autofit();
}
#Override
public void afterTextChanged(Editable editable) {
// do nothing
}
}
private class AutofitOnLayoutChangeListener implements View.OnLayoutChangeListener {
#Override
public void onLayoutChange(View view, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
autofit();
}
}
/**
* When an object of a type is attached to an {#code AutofitHelper}, its methods will be called
* when the {#code textSize} is changed.
*/
public interface OnTextSizeChangeListener {
/**
* This method is called to notify you that the size of the text has changed to
* {#code textSize} from {#code oldTextSize}.
*/
public void onTextSizeChange(float textSize, float oldTextSize);
}
}
Then add the needed attributes in
values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="AutofitButton">
<!-- Minimum size of the text. -->
<attr name="minTextSize" format="dimension" />
<!-- Amount of precision used to calculate the correct text size to fit within its
bounds. Lower precision is more precise and takes more time. -->
<attr name="precision" format="float" />
<!-- Defines whether to automatically resize text to fit to the view's bounds. -->
<attr name="sizeToFit" format="boolean" />
</declare-styleable>
<!-- Your other attributes -->
</resources>
And you're done! You can now use the AutoSizeTextButton class.
<your.package.name.AutoSizeTextButton
android:layout_width="..."
android:layout_height="..."
android:maxLines="2" />
And be sure to add the android:maxLines attribute with a value larger than 0, otherwise it won't do anything!
Additional Notes:
The text is shrunken until the longest word fits into the button without wrapping (or the minimum size is reached). The words have to be seperated by either a normal space, or a hyphen. This algorithm also considers a SOFT HYPHEN or a ZERO WIDTH SPACE a word seperator, however I would strongly advise to test them before using them, because the Android Text Engine used in buttons ignores these characters (at least in API 19), which could lead to weird word-wraps.
It's better you use this library named AutoScaleTextView
https://bitbucket.org/ankri/autoscaletextview
This will definitely help you to achieve your desired task.
There is no built-in way of doing this, you will need to create/use a custom view that adapts the inner text to its bounds. Don't worry, this is not the first time its been asked, see the Custom View provided at Auto Scale TextView Text to Fit within Bounds to get the working code.
if any one is looking on how to disable auto text size, it can be done by
<TextView
app:autoSizeTextType="none" <!-- disabled -->
adding the above line to your text view
So the problem I am facing right now is the following, I want to create a fisheye view (so a view which contains items while the item in the middle is bigger then the other ones like you will see in the MAC OS itembar or somewhere else).
So far I have extended the HorizontalScrollView to achieve that and after some testing everything seemed to work fine, so when moving the scrollview the items are updated properly depending on their position.
However there is one problem that occurs if the scrollview "bounces" against its borders. So if the ScrollView moves to fast the "getScrollX()" will give me a value that is smaller 0 or bigger then the max bounds. After that happens the items are no longer resized, which is quite strange.
I checked my code and the resize methods of my items are called, yet i don´t know why the items are no longer updated.
The ScrollView class looks like the following
public class HorizontalFishEyeView extends HorizontalScrollView
{
//****************************************************************************************************
// enums
//****************************************************************************************************
public enum ONTOUCHEND
{
//-----undefined value
NONE,
//-----meaning to keep scrolling after the touch event ended
SCROLL_ON_END,
//-----meaning to continue to the next base after the touch event ended
CONTINUE_ON_END,
//-----meaning to switch the element after the on touch event ended
CHANGE_ELEMENT_ON_END
}
public enum MODE
{
//-----none mode meaning the view is not scrolling or doing the finish animation, thus being idle
NONE,
//-----scroll meaning the view is scrolling after an accelleration
SCROLL,
//-----finish meaning the view is doing the finish animation to move to the actual element
FINISH,
}
//****************************************************************************************************
// variables
//****************************************************************************************************
//-----determines if the view will continue when the finish animation is played
private boolean m_bContinueOnClick = false;
//-----time for the scroll animation
private long m_nScrollAnimationTime = 0;
//------the multiplier to be used for the velocity on initaial start
private float m_fVelocityMultiplier = 1.0f;
//-----the direction of the velocity however the reverse value to use it in conjunction with the decrement
private int m_nVelocityDirectionReverse = 0;
//------the velocity provided when the event has ended
private float m_fVelocity = 0.0f;
//-----determines hwo much the velocity decreases per millisecond
private float m_fVelocityDecrement = 0.001f;
//-----time when the touch event was started
private long m_nStartTime = 0;
//-----the x position of the touch event
private float m_nXPosition = -1.0f;
//-----determines when the animation for moving shall be canceled
private final float m_fVelocityThreshold = 0.25f;
//-----determines the time, e.g the start time of the animation and stores the time each time the draw method is called
//-----while the finish animation is in progress
private long m_nFinishAnimationTime = 0;
//-----determines how much pixel the layout will be moved for each millisecond passed,
//-----while the finish animation is playing
private double m_dFinishAnimationIncrements = 0.0;
//-----the actually duration of the finish animation, this value is dependent of the difference distance
//-----which the view has to be moved, so at max this will bei 0.5 times m_nFinishAnimationTime
private int m_nFinishAnimationDuration = 0;
//-----determines the distance which the view has to be moved in order to set the selected element into focus
private int m_nFinishRemainingDiff = 0;
//-----the position which the view will have as its left margin,
//-----this value us determined when the user lets go of the view
private int m_nFinishTargetPosition = 0;
//-----the animation time the finish animation when the user lets go of the view
private int m_nAnimationTime = 0;
//-----the position of the element which is closest to the selector, thus it actually is the selected element
private FishEyeItem m_nClosestElement = null;
//-----scalefactor used to calculate the min item size, thus m_nItemSizeMin = nItemSize * m_fItemSizeMaxScale
private float m_fItemSizeMinScale = -1;
//-----the size of the image of the item when not selected
private int m_nItemSizeMin = -1;
//-----scalefactor used to calculate the max item size, thus m_nItemSizeMax = nItemSize * m_fItemSizeMaxScale
private float m_fItemSizeMaxScale = -1;
//-----the size of the image of the item when selected
private int m_nItemSizeMax = -1;
//-----the difference in item size between the max and the min value
private int m_nItemSizeDiff = -1;
//-----determines at which distance the item size will always be min
private int m_nMaxDiff = 0;
//-----the middel point of the view, used to determine the distance of an item and thus its size
private int m_nReferenceX = 0;
//-----event listener attached to this view
protected AnimationEventListener m_oEventListener;
//-----this factor is multiplied by the velocity up on the UP event to determine the remaining scroll
private float m_fVelocityScaleFactor = 0.25f;
//-----the mode in which the fisheyeview currently is
private MODE m_eMode = MODE.NONE;
//-----the reference to the one and only child in the scrollview, as it should be
private LinearLayout m_oChild = null;
//-----number of items whose bitmap will still be available even if they are not visible
private int m_nItemBuffer = 2;
//-----activity to use
private Activity m_oActivity = null;
//-----scalefactor to use
private float m_fScaleFactor = 1.0f;
//-----determines if the itemsize is stable thus each item is the same size, used to prevent unnecessary calculations
private boolean m_bItemSizeStable = false;
//****************************************************************************************************
// constructor
//****************************************************************************************************
public HorizontalFishEyeView(Context context)
{
super(context);
}
public HorizontalFishEyeView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
//****************************************************************************************************
// public
//****************************************************************************************************
/**
* this method will set up the view before it is used, thus it needs to be called before
* #param oActivity
* #param fItemSizeMinScale
* #param fItemSizeMaxScale
* #param fScaleFactor
* #param bItemSizeStable
*/
public void Initialize(Activity oActivity, float fItemSizeMinScale, float fItemSizeMaxScale, float fScaleFactor, boolean bItemSizeStable)
{
try
{
m_oActivity = oActivity;
m_nReferenceX = (int)(getWidth()*0.5f);
m_fItemSizeMaxScale = Math.min(1.0f, Math.max(0.0f, fItemSizeMaxScale));
m_fItemSizeMinScale = Math.min(1.0f, Math.max(0.0f, fItemSizeMinScale));
m_bItemSizeStable = bItemSizeStable;
m_fScaleFactor = fScaleFactor;
}
catch(Exception e)
{
Log.d("Initialize", e.toString());
}
}
public void Clear()
{
try
{
if(m_oChild!=null)
{
for(int i=0;i<m_oChild.getChildCount();i++)
{
View oChild = m_oChild.getChildAt(i);
if(oChild instanceof FishEyeItem)
{
NetcoMethods.RecycleImageView(((FishEyeItem)oChild).GetImageView());
}
}
m_oChild.removeAllViews();
}
}
catch(Exception e)
{
Log.d("Clear", e.toString());
}
}
public void AddItem(FishEyeItem oItem, LinearLayout.LayoutParams oParams)
{
try
{
if(m_oChild!=null)
{
m_oChild.addView(oItem, oParams);
}
}
catch(Exception e)
{
Log.d("AddItem", e.toString());
}
}
public MODE GetMode()
{
return m_eMode;
}
public void Reinitialize()
{
}
public void Deinitialize()
{
}
/**
* adds an animation listener to the list
* #param listener
*/
public void SetAnimationEventListener(AnimationEventListener listener)
{
m_oEventListener = listener;
}
public void ScrollTo()
{
try
{
}
catch(Exception e)
{
Log.d("ScrollTo", e.toString());
}
}
public LinearLayout GetChild()
{
return m_oChild;
}
//****************************************************************************************************
// private
//****************************************************************************************************
/**called when the size was calculated*/
private void SizeCalculated(Object o)
{
try
{
if(m_oEventListener!=null)
{
m_oEventListener.AnimationEvent(o);
}
}
catch(Exception e)
{
Log.d("AnimationEndEvent", e.toString());
}
}
/**
* calculates the sizes for an item, if m_bItemSizeStable is set to true this will only be done once
* #param nItemSize, the size of the item which will be used
*/
private void CalulateItemSize(int nItemSize)
{
try
{
if(!m_bItemSizeStable)
{
m_nItemSizeMax = (int)(nItemSize * m_fItemSizeMaxScale);
m_nItemSizeMin = (int)(nItemSize * m_fItemSizeMinScale);
m_nItemSizeDiff = m_nItemSizeMax - m_nItemSizeMin;
m_nMaxDiff = nItemSize*2;
}
else if(m_nItemSizeMax==-1)
{
m_nItemSizeMax = (int)(nItemSize * m_fItemSizeMaxScale);
m_nItemSizeMin = (int)(nItemSize * m_fItemSizeMinScale);
m_nItemSizeDiff = m_nItemSizeMax - m_nItemSizeMin;
m_nMaxDiff = nItemSize*2;
}
}
catch(Exception e)
{
Log.d("CalculateItemSize", e.toString());
}
}
/**
* this method will Resize and item in the view depending on its position
* #param oItem the item which shall be resized
* #param nDiff the distance of the item from the middle of he view
* #param nCurrentClosestDiff the currently know closest distance, if the item is closer the given nDiff will be used
*/
private void DeterminenSize(FishEyeItem oItem, int nDiff, int nCurrentClosestDiff)
{
try
{
if(oItem!=null)
{
CalulateItemSize(oItem.getWidth());
//-----check if the item can be resized
if(oItem.GetCanBeResized())
{
//System.out.println("Item is "+ oItem.GetImagePath());
//System.out.println("Item Diff is "+ nDiff);
//-----items is in range
if(nDiff<m_nMaxDiff)
{
//-----determine whether this element is closer to the selector then the previously known
if(nCurrentClosestDiff==-1)
{
nCurrentClosestDiff = nDiff;
m_nClosestElement = oItem;
SizeCalculated(m_nClosestElement);
}
else
{
if(nDiff<nCurrentClosestDiff)
{
nCurrentClosestDiff = nDiff;
m_nClosestElement = oItem;
SizeCalculated(m_nClosestElement);
}
}
//-----get the new size
float fRelative = 1.0f - (float)nDiff/(float)m_nMaxDiff;
int nNewItemSize = m_nItemSizeMin + (int)(fRelative * m_nItemSizeDiff);
//-----set the new size
oItem.Resize(nNewItemSize, nNewItemSize);
oItem.SetIsInRange(true);
}
else
{
//----if the item is now out of range set it to the minimum size
if(oItem.GetIsInRange())
{
//-----set the minimum size
oItem.Resize(m_nItemSizeMin, m_nItemSizeMin);
oItem.SetIsInRange(false);
}
}
}
}
}
catch(Exception e)
{
Log.d("DeterminenSize", e.toString());
}
}
//****************************************************************************************************
// overrides
//****************************************************************************************************
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
{
super.onScrollChanged(l, t, oldl, oldt);
try
{
if(m_eMode == MODE.FINISH)
{
}
else
{
//------get the top element which must be a linear layout
if(m_oChild!=null)
{
m_oChild.setWillNotDraw(false);
FishEyeItem oFishEyeItem = null;
View oChildView = null;
ImageView oImage = null;
String cFilename = null;
int nPositionStart = 0;
int nPositionEnd = 0;
int nItemSize = 0;
int nScroll = getScrollX();
int nBoundEnd = getWidth();
int nItemPosition = 0;
int nCurrentClosestDiff = -1;
System.out.println(nScroll);
for(int i=0;i<m_oChild.getChildCount();i++)
{
oChildView = m_oChild.getChildAt(i);
//-----check if the child is of a certain type
if(oChildView instanceof FishEyeItem)
{
oFishEyeItem = (FishEyeItem)oChildView;
nItemSize = oFishEyeItem.getWidth();
nPositionStart = i * nItemSize;
nPositionEnd = nPositionStart + nItemSize;
oImage = oFishEyeItem.GetImageView();
cFilename = oFishEyeItem.GetImagePath();
//-----check if the item is in visible area
if(oImage!=null)
{
//-----image is in visible area
if(nPositionEnd>=nScroll - (m_nItemBuffer * nItemSize) && nPositionStart - (m_nItemBuffer * nScroll)<=nBoundEnd)
{
//-----check if image needs to be loaded
if(!oFishEyeItem.GetIsImageLoaded())
{
oFishEyeItem.SetIsImageLoaded(true);
new DownloadTaskImage(m_oActivity,
oImage,
cFilename,
nItemSize,
nItemSize,
m_fScaleFactor,
POWERROUNDMODES.ROUND).execute((Void)null);
}
//-----get the item position in the fisheyeview
nItemPosition = nPositionStart - nScroll + (int)(nItemSize*0.5f);
DeterminenSize(oFishEyeItem, Math.abs(m_nReferenceX - nItemPosition), nCurrentClosestDiff);
}
else
{
//-----check if an image can be recycle
if(oFishEyeItem.GetIsImageLoaded())
{
oFishEyeItem.SetIsImageLoaded(false);
new RecycleTaskImage(oImage).execute((Void)null);
}
}
}
}
}
}
}
}
catch(Exception e)
{
Log.d("onScrollChanged", e.toString());
}
}
#Override
public boolean onTouchEvent(MotionEvent oEvent)
{
super.onTouchEvent(oEvent);
try
{
switch(oEvent.getAction())
{
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
break;
default:
break;
}
}
catch(Exception e)
{
Log.d("onTouchEvent", e.toString());
}
return true;
}
protected void onFinishInflate()
{
super.onFinishInflate();
try
{
m_oChild = (LinearLayout)getChildAt(0);
}
catch(Exception e)
{
Log.d("onFinishInflate", e.toString());
}
}
}
Don´t mind some of the unused variables as they are intended to be used later to realize an autoscroll feature once the view itself has finished scrolling (so that the currently closed item will always be in the middle up on letting go of the scrollview).
The View requires to actually be filled with "FishEyeItem"s, which are then used to load images and resize the content. Those Items are loaded during runtime after I have gained the list of items that i need to display.
The FishEyeItem code is the following.
public class FishEyeItem extends RelativeLayout
{
//****************************************************************************************************
// variables
//****************************************************************************************************
//-----determines if this item can be resized
private boolean m_bCanBeResized = false;
//-----path to the image of this fisheye items image
private String m_cImagePath = null;
//-----determines if this item is in range for the fisheye calculation
private boolean m_bIsInRange = true;
//-----determines if the image is loaded already, thus occupying memory
private boolean m_bIsImageLoaded = false;
//-----id of the image4view holding the image
private int m_nImageViewID = -1;
//-----the id of the view in this view which is responsible for resizing
private int m_nResizeViewID = -1;
//****************************************************************************************************
// constructor
//****************************************************************************************************
public FishEyeItem(Context context)
{
super(context);
}
public FishEyeItem(Context context, AttributeSet attrs)
{
super(context, attrs);
}
//****************************************************************************************************
// setter
//****************************************************************************************************
public void SetCanBeResized(boolean bValue)
{
m_bCanBeResized = bValue;
}
public void SetImagePath(String cValue)
{
m_cImagePath = cValue;
}
public void SetIsInRange(boolean bValue)
{
m_bIsInRange = bValue;
}
public void SetIsImageLoaded(boolean bValue)
{
m_bIsImageLoaded = bValue;
}
public void SetImageViewID(int nValue)
{
m_nImageViewID = nValue;
}
public void SetResizeViewID(int nValue)
{
m_nResizeViewID = nValue;
}
//****************************************************************************************************
// getter
//****************************************************************************************************
public boolean GetCanBeResized()
{
return m_bCanBeResized;
}
public String GetImagePath()
{
return m_cImagePath;
}
public boolean GetIsInRange()
{
return m_bIsInRange;
}
public boolean GetIsImageLoaded()
{
return m_bIsImageLoaded;
}
public int GetImageViewID()
{
return m_nImageViewID;
}
public int GetResizeViewID()
{
return m_nResizeViewID;
}
public ImageView GetImageView()
{
ImageView oView = null;
try
{
oView = (ImageView)findViewById(m_nImageViewID);
}
catch(Exception e)
{
Log.d("GetImageView", e.toString());
}
return oView;
}
//****************************************************************************************************
// getter
//****************************************************************************************************
public void Resize(int nWidth, int nHeight)
{
try
{
View oView = findViewById(m_nResizeViewID);
if(oView!=null)
{
System.out.println("Resizing Item" + m_cImagePath);
//-----set the minimum size
RelativeLayout.LayoutParams oParams = (RelativeLayout.LayoutParams)oView.getLayoutParams();
oParams.width = nWidth;
oParams.height = nHeight;
oView.setLayoutParams(oParams);
}
}
catch(Exception e)
{
Log.d("Resize", e.toString());
}
}
}
So essentially everytime the onScrollChanged() is called, the image of an item will be loaded or recycled (both are in running in a async task so they dont block the scrolling and GUI). Also the size of an item will be determined if it is a certain distance away from the middle of the scrollview.
Like I said the Resize() method always gets called (thats why the system.out is there) but when "bouncing" against the borders the items are no longer resized.
So I am guessing the problem is somewhere in the HorizontalScrollView class itself, e.g. a certain flag gets set when "bouncing" against the borders.
EDIT:
So okay I was able to prevent that the items were not able to be updated anymore by simply checking the getScrollX() in the onscrollchanged() and returning if that value is <= 0 or if the value is >= the max bounds. However this still does not explain the fact that the items were not updated anymore when "bouncing" against the borders.