Android: Custom Horizontal Progress Bar Animation

2019-02-01 06:19发布

问题:

I'm trying to create a progress bar where the bar itself animates in a vertical spin as it progresses horizontally. I'm successfully using my progress drawable as the drawable via:

<ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:progressDrawable="@drawable/custom_progress"
        android:layout_marginRight="5dp" />

Here is my drawable:

But I want it to have a subtle roll effect as its progressing. So it would look like the vertical lines are moving backwards sorta. You follow? Any help is much appreciated. Thanks.

EDIT: I tried created an animation-list as my progress drawable but I'm still not able to see the animation. Can an animation-list be inside of a clip for the progress item?

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background" android:drawable="@drawable/gutter"></item>

<item android:id="@android:id/progress">

<clip>
    <animation-list android:oneshot="false">
        <item android:drawable="@drawable/progress_bar_animate" android:duration="100" />
        <item android:drawable="@drawable/progress_bar_animate2" android:duration="100" />
        <item android:drawable="@drawable/progress_bar_animate3" android:duration="100" />
        <item android:drawable="@drawable/progress_bar_animate4" android:duration="100" />
        <item android:drawable="@drawable/progress_bar_animate5" android:duration="100" />
        <item android:drawable="@drawable/progress_bar_animate6" android:duration="100" />
        <item android:drawable="@drawable/progress_bar_animate7" android:duration="100" />
    </animation-list>
</clip>

</item>
</layer-list>

回答1:

Yahoo! Finally ! Works ! (BUT !!!! can cause memory leak with big images. It's fixed in the post below)

This code takes the tile picture (tile1), repeats it(TileMode.REPEAT), makes shifted animation (10 fragments), adds this in Animation set.

private void initAnimation() {
//      R.drawable.tile1 is PNG
        Bitmap b = BitmapFactory.decodeResource(getResources(),R.drawable.tile1);
        AnimationDrawable shiftedAnimation = getAnimation(b);

//      R.id.img_3 is ImageView in my application
        View v = findViewById(R.id.img_3);
        v.setBackground(shiftedAnimation);
        shiftedAnimation.start();
}

private Bitmap getShiftedBitmap(Bitmap bitmap, int shiftX) {
    Bitmap newBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
    Canvas newBitmapCanvas = new Canvas(newBitmap);

    Rect srcRect1 = new Rect(shiftX, 0, bitmap.getWidth(), bitmap.getHeight());
    Rect destRect1 = new Rect(srcRect1);
    destRect1.offset(-shiftX, 0);
    newBitmapCanvas.drawBitmap(bitmap, srcRect1, destRect1, null);

    Rect srcRect2 = new Rect(0, 0, shiftX, bitmap.getHeight());
    Rect destRect2 = new Rect(srcRect2);
    destRect2.offset(bitmap.getWidth() - shiftX, 0);
    newBitmapCanvas.drawBitmap(bitmap, srcRect2, destRect2, null);

    return newBitmap;
}

private List<Bitmap> getShiftedBitmaps(Bitmap bitmap) {
    List<Bitmap> shiftedBitmaps = new ArrayList<Bitmap>();
    int fragments = 10;
    int shiftLength = bitmap.getWidth() / fragments;

    for(int i = 0 ; i < fragments; ++i){
        shiftedBitmaps.add( getShiftedBitmap(bitmap,shiftLength * i));
    }

    return shiftedBitmaps;
}

private AnimationDrawable getAnimation(Bitmap bitmap) {
    AnimationDrawable animation = new AnimationDrawable();
    animation.setOneShot(false);

    List<Bitmap> shiftedBitmaps = getShiftedBitmaps(bitmap);
    int duration = 50;

    for(Bitmap image: shiftedBitmaps){
        BitmapDrawable navigationBackground = new BitmapDrawable(getResources(), image);
        navigationBackground.setTileModeX(TileMode.REPEAT);

        animation.addFrame(navigationBackground, duration);
    }
    return animation;
}


回答2:

I don't think this can be done the way I thought it could. The reason being that I can't reference the animated-list, which at that point is a ClipDrawable, and cast it to an AnimateDrawable which is necessary so I can programmatically start the animation. It's also necessary to contain it in the clip element as that's the ProgressBar's way of masking an image so it only displays part of the image as its progressing.

Aside from faking my own progress bar using ImageViews and animating those I don't see another way around it for this specific ProgressBar.



回答3:

My previous method had problem with memory consume. I had improved it to use TranslateAnimation. BUT it has little LAG (pay attention on HACK comment in ProgressBgView.initAnimation method)

Layout xml :

    <com.example.tweenanimation.ProgressBgView
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:id="@+id/progress_db_view"
        />

Backend :

    final ProgressBgView prog = (ProgressBgView) findViewById(R.id.progress_db_view);
        prog.setBackgroundAsTile(R.drawable.bg_tile_1);
        prog.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                prog.startAnimation();
            }
        });

And the class :

public class ProgressBgView extends FrameLayout {
    private ImageView mProgressImage;
    private TranslateAnimation mProgressAnimation;

    public ProgressBgView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mProgressImage = new ImageView(getContext());
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        mProgressImage.setLayoutParams(layoutParams);
        addView(mProgressImage);
    }

    public void setBackgroundAsTile(int tileImageResId) {
        Bitmap tileBitmap = BitmapFactory.decodeResource(getResources(), tileImageResId);
        BitmapDrawable tileRepeatedBitmap = new BitmapDrawable(getResources(), tileBitmap);
        tileRepeatedBitmap.setTileModeX(TileMode.REPEAT);

        initAnimation(tileBitmap.getWidth());

        mProgressImage.setBackgroundDrawable(tileRepeatedBitmap);
    }

    private void initAnimation(int tileImageWidth) {
        FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mProgressImage.getLayoutParams();
        layoutParams.setMargins(-tileImageWidth, 0, 0, 0);

        // *HACK* tileImageWidth-3 is used because of *lags*(slow pause) in the moment
        // of animation END-RESTART.
        mProgressAnimation = new TranslateAnimation(0, tileImageWidth - 3, 0, 0);
        mProgressAnimation.setInterpolator(new LinearInterpolator());
        mProgressAnimation.setDuration(1000);
        mProgressAnimation.setRepeatCount(Animation.INFINITE);
    }

    public void startAnimation() {
        mProgressImage.startAnimation(mProgressAnimation);
    }
}


回答4:

This what I get.

And this is way:

<ProgressBar
    android:layout_width="match_parent"
    android:layout_height="15dp"
    android:indeterminate="true"
    style="@style/IndeterminateProgressBar" />

After that in styles.xml:

<style name="IndeterminateProgressBar" parent="@android:style/Widget.ProgressBar.Horizontal">
    <item name="android:indeterminateDrawable">@anim/progress_bar_indeterminate</item>
</style>

In file res/anim/progress_bar_indeterminate.xml:

<?xml version="1.0" encoding="utf-8"?>
<animation-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/progress_bar_horizontal_1" android:duration="200" />
    <item android:drawable="@drawable/progress_bar_horizontal_2" android:duration="200" />
    <item android:drawable="@drawable/progress_bar_horizontal_3" android:duration="200" />
    <item android:drawable="@drawable/progress_bar_horizontal_4" android:duration="200" />
</animation-list>

And here 4 images that need to put in folder res/drawable:

P.S. If you need reverse direction of the progress bar, just flip the numbers in name of images in progress_bar_indeterminate.xml.

Good day! =)



回答5:

I found a solution. Create an xml say, progress_loading.xml as follows and assign it to the progress drawable of the progressbar :

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
  <item android:id="@android:id/progress">
    <clip>
      <shape>
        <solid android:color="#33FFFFFF" />
      </shape>
    </clip>
  </item>
</layer-list>

Then in code:

AnimationDrawable anim = (AnimationDrawable) getResources()
                .getDrawable(R.drawable.loading);
        ClipDrawable clipDrawable = new ClipDrawable(anim, Gravity.LEFT,
                ClipDrawable.HORIZONTAL);
        LayerDrawable layerDrawable = (LayerDrawable) progressBar
                .getProgressDrawable();
        layerDrawable.setDrawableByLayerId(android.R.id.progress,
                clipDrawable);
        anim.start();

Where the xml loading.xml has the actual animation-list. This is worked for me.



回答6:

I found one solution, kind of similar to above explanation but not originated from it. Works great!

Say below is our progress_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <solid
                android:color="@color/black_20"/>
        </shape>
    </item>

    <item android:id="@android:id/progress">
        <clip>
            <animation-list
                android:oneshot="false">
                <item
                    android:drawable="@drawable/stripe3"
                    android:duration="100"/>
                <item
                    android:drawable="@drawable/stripe2"
                    android:duration="100"/>
                <item
                    android:drawable="@drawable/stripe1"
                    android:duration="100"/>
            </animation-list>
        </clip>
    </item>
</layer-list>

then in Java code, do the following:

mDownloadProgressBar.setProgressDrawable(getResources().getDrawable(R.drawable.download_progress, null));
LayerDrawable drawable = (LayerDrawable)mDownloadProgressBar.getProgressDrawable();
ClipDrawable clipDrawable = (ClipDrawable) drawable.getDrawable(1);
AnimationDrawable animationDrawable =(AnimationDrawable)clipDrawable.getDrawable();
animationDrawable.start();


回答7:

download_progress_anim.xml

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@drawable/stripe3"
        android:duration="100"/>
    <item
        android:drawable="@drawable/stripe2"
        android:duration="100"/>
    <item
        android:drawable="@drawable/stripe1"
        android:duration="100"/>
</animation-list>

Java code:

mDownloadProgressBar.setProgressDrawable(createProgressDrawable(context));

private Drawable createProgressDrawable(Context context) {

    ShapeDrawable shape = new ShapeDrawable();
    shape.getPaint().setStyle(Paint.Style.FILL);
    shape.getPaint()
            .setColor(ContextCompat.getColor(context, R.color.black_20));

    AnimationDrawable animationDrawable = (AnimationDrawable) getDrawable(R.drawable.download_progress_anim, context);
    ClipDrawable clipDrawable = new ClipDrawable(animationDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
    animationDrawable.start();

    LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{shape, clipDrawable});
    layerDrawable.setId(0, android.R.id.background);
    layerDrawable.setId(1, android.R.id.progress);
    return layerDrawable;
}

private Drawable getDrawable(@DrawableRes int drawable, Context context) {
        if (Build.VERSION.SDK_INT < 21) {
            return context.getResources().getDrawable(drawable);
        } else {
            return context.getResources().getDrawable(drawable, null);
        }
    }

Hope it helps. You can easily modify it to support your own implementation.



回答8:

For those who want to make progress bar determinate, I post my improvement to user1269737's answer.

I've changed intAnimation method like this:

private void initAnimation(int tileImageResId) {
    Bitmap b = BitmapFactory.decodeResource(getResources(), tileImageResId);
    if (b != null) {
        shiftedAnimation = getAnimation(b);

        setBackgroundColor(ContextCompat.getColor(getContext(), #14000000));
        mProgressImage.setBackground(shiftedAnimation);
    }
}

And added setProgress method with animated progress change:

 public void setProgress(double progress){
    if (progress > 100){
        progress = 100;
    }
    else if (progress < 0){
        progress = 0;
    }
    int parentWidth = getWidth();
    final int newWidth =  (int)(parentWidth*(progress/100.0));

    widthAnimator = ValueAnimator.ofInt(mProgressImage.getLayoutParams().width, newWidth);
    widthAnimator.setDuration(200);
    widthAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mProgressImage.getLayoutParams().width = (Integer) animation.getAnimatedValue();
            mProgressImage.requestLayout();
        }
    });
    widthAnimator.start();
}

And some changes in constructor:

public LProgressBar(Context context, AttributeSet attrs) {
    super(context, attrs);

    mProgressImage = new ImageView(getContext());
    FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT);
    mProgressImage.setLayoutParams(layoutParams);
    addView(mProgressImage);

    initAnimation(R.drawable.pb_background);
}