居中显示并旋转 android Button 里的属性drawableLeft

如图,点击同步按钮,同步图片要旋转起来,直到同步完毕。有一个容易实现的方法,就叫“方法1”吧(下面会用的),一个LinearLayout里面包含一个ImageView和一个TextView并且居中显示,监听LinearLayout的点击事件,然后旋转ImageView。

<LinearLayout
        android:id="@+id/syn_now_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/address_clipboard_item_divider_padding"
        android:layout_marginLeft="@dimen/login_activity_padding"
        android:layout_marginRight="@dimen/login_activity_padding"
        android:layout_marginBottom="@dimen/address_clipboard_item_divider_padding"
        android:background="@color/download_apk_scanning"
        android:orientation="horizontal"
        android:gravity="center"
        android:paddingTop="10dp"
        android:paddingBottom="10dp"
        >
        <ImageView
            android:id="@+id/sync_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/syn"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:duplicateParentState="true"
            android:textSize="15sp"
            android:textColor="@color/white"
            android:text="@string/sync_now"/>

    </LinearLayout>

    mSyncImage = (ImageView)findViewById(R.id.sync_image);
    findViewById(R.id.syn_now_layout).setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
                mSyncImage.clearAnimation();
                mSyncImage.startAnimation(getRotateAnimtion());
        }
    });

    /**
     * @return 旋转动画
     */
    private RotateAnimation getRotateAnimtion(){
        if(rAnimation==null)
        {
            rAnimation = new RotateAnimation(0, 359, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
            rAnimation.setDuration(1000);
            rAnimation.setRepeatCount(-1);
            rAnimation.setInterpolator(new LinearInterpolator());
        }
        return rAnimation;
    }



但是,在我知道要实现这个功能的时候,第一反应是通过Button的drawableLeft属性实现(一个按钮上同时显示图片和文字),我想没有深入了解过drawableLeft属性的人(比如我)大部分都会是我这个想法的。真的去实现的时候才发现,我擦,居然有两大难点,一是从Button中取出图片的drawable,不知道怎么对drawable对象做动画,一是drawableLeft 图片不居中显示


1. drawableLeft 取出来后,怎么旋转呢

通过Google知道系统的圆形进度条的图片是spinner_white_16.png,用到sdk/platforms/android-22/data/res/drawable/progress_small_white.xml文件

<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/spinner_white_16"
    android:pivotX="50%"
    android:pivotY="50%"
    android:framesCount="12"
    android:frameDuration="100" />
模仿写试了试发现, android:framesCount与android:frameDuratiion 是内部属性,不能用。

根据animated-rotate我又找到AnimatedRotateDrawable类

/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.graphics.drawable;

import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.ColorFilter;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.util.Log;
import android.os.SystemClock;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;

import com.android.internal.R;

/**
 * @hide
 */
public class AnimatedRotateDrawable extends Drawable implements Drawable.Callback, Runnable,
        Animatable {

    private AnimatedRotateState mState;
    private boolean mMutated;
    private float mCurrentDegrees;
    private float mIncrement;
    private boolean mRunning;

    public AnimatedRotateDrawable() {
        this(null, null);
    }
·······
}
看到了吧,又是隐藏类。但是我又看到了 AnimatedRotateDrawable实现了Animatable接口。

好像可以试一试,在工程的drawable文件下创建文件animated_rotate.xml

<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/syn"
    android:pivotX="50%"
    android:pivotY="50%" />
然后

  <Button
    android:id="@+id/sync_now" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:drawableLeft="@drawable/animated_rotate"
    android:text="立即同步"/>

在Button的点击事件里处理旋转问题

	public void onClick(View v) {

		Button btnButton = (Button) v;
		// 获取android:drawableLeft
		Drawable drawable = btnButton.getCompoundDrawables()[0];
		if (!((Animatable) drawable).isRunning()) {
			((Animatable) drawable).start();
		} else {
			((Animatable) drawable).stop();
		}
	}

      2.drawableLeft 图片不居中显示:

解决办法两种,一种是Button的android:layout_width和android:layout_height的属性设为wrap_content,然后外面在包一层LinearLayout ,设置属性 android:gravity="center",然后监听LinearLayout点击事件,咦,这不和“方法1”一样了吗,换第二种方法 ,第二种方法就是自定义控件了。通过网络查到两种结局办法,

 一种是文字图片都在左边,然后在 onDraw 函数里向右平移画布(canvas.translate),调用父级onDraw去画。 

 一种是自己写onDraw方法,在Canvas画布的某个位置去画图和文字,不在调用父级onDraw。这个种方式比上一个麻烦,但是实现了normal状态和press状态的图片切换。 

两种方式都试过之后,图片和文字都居中显示了,但是当点击按钮后出了一个问题,图片不旋转了。去掉我在onDraw里写的代码,直接调用super.onDraw(canvas) 图片旋转就没问题。通过查看AnimatedRotateDrawable 和 TextView的源码才明白我应该重写public void invalidateDrawable(Drawable drawable) 方法。(Button的父类是TextView,drawableLeft的属性就是在TextView里实现的)

首先看TextView里的onDraw是怎么实现的,其中有段代码是画drawableLeft的

            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
            // Make sure to update invalidateDrawable() when changing this code.
            if (dr.mDrawableLeft != null) {
                canvas.save();
                canvas.translate(scrollX + mPaddingLeft + leftOffset,
                                 scrollY + compoundPaddingTop +
                                 (vspace - dr.mDrawableHeightLeft) / 2);
                dr.mDrawableLeft.draw(canvas);
                canvas.restore();
            }

没有啥区别呀,先保存画布的状态,然后平移画布,画上图片,再恢复画布平移前的保存的状态。注意看上面的两段英文注释,虽然看到了invalidateDrawable函数,但是因为不了解就给忽略了。没有办法了,只能从动画开始一步一步分析吧。

动画开始,调用start() 方法,AnimatedRotateDrawable文件

    @Override
    public void start() {
        if (!mRunning) {
            mRunning = true;
            nextFrame();
        }
    }
    private void nextFrame() {
        unscheduleSelf(this);
        scheduleSelf(this, SystemClock.uptimeMillis() + mState.mFrameDuration);
    }
unscheduleSelf scheduleSelf 这两个函数干嘛?去父类Drawable看看实现

    public void unscheduleSelf(Runnable what) {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.unscheduleDrawable(this, what);
        }
    public void scheduleSelf(Runnable what, long when) {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.scheduleDrawable(this, what, when);
        }
    }

getCallback,那就有setCallback,Drawable的setCallback是在是在什么时候调用的呢,TextView 设置drawableLeft的函数是    public void setCompoundDrawables(Drawable left, Drawable top, right, Drawable bottom),其中有段代码是

            if (left != null) {
                left.setState(state);
                left.copyBounds(compoundRect);
                left.setCallback(this);
                dr.mDrawableSizeLeft = compoundRect.width();
                dr.mDrawableHeightLeft = compoundRect.height();
            } else {
                dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
            }

unscheduleDrawable  scheduleDrawable 的实现函数应该就在TextView里,查找后在父类里发现了实现方法

  /**
     * Schedules an action on a drawable to occur at a specified time.
     *
     * @param who the recipient of the action
     * @param what the action to run on the drawable
     * @param when the time at which the action must occur. Uses the
     *        {@link SystemClock#uptimeMillis} timebase.
     */
    public void scheduleDrawable(Drawable who, Runnable what, long when) {
        if (verifyDrawable(who) && what != null) {
            final long delay = when - SystemClock.uptimeMillis();
            if (mAttachInfo != null) {
                mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
                        Choreographer.CALLBACK_ANIMATION, what, who,
                        Choreographer.subtractFrameDelay(delay));
            } else {
                ViewRootImpl.getRunQueue().postDelayed(what, delay);
            }
        }
    }

    /**
     * Cancels a scheduled action on a drawable.
     *
     * @param who the recipient of the action
     * @param what the action to cancel
     */
    public void unscheduleDrawable(Drawable who, Runnable what) {
        if (verifyDrawable(who) && what != null) {
            if (mAttachInfo != null) {
                mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
                        Choreographer.CALLBACK_ANIMATION, what, who);
            } else {
                ViewRootImpl.getRunQueue().removeCallbacks(what);
            }
        }
    }


有没有发现什么,scheduleDrawable  函数中有ViewRootImpl.getRunQueue().postDelayed(what, delay);根据名字就能判断这是多长时间之后执行Runnable,也就是执行AnimatedRotateDrawable文件里的run()函数(不理解的查一下Runnable是个啥)。

因为what参数是AnimatedRotateDrawable类里scheduleSelf函数的第一个参数this,run函数实现

    @Override
    public void run() {
        // TODO: This should be computed in draw(Canvas), based on the amount
        // of time since the last frame drawn
        mCurrentDegrees += mIncrement;
        if (mCurrentDegrees > (360.0f - mIncrement)) {
            mCurrentDegrees = 0.0f;
        }
        invalidateSelf();
        nextFrame();
    }

nextFrame函数,又是一个循环。剩下的关键函数就是invalidateSelf,刷新,实现函数还是TextView里,因为Drawable里的实现方式是

    public void invalidateSelf() {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.invalidateDrawable(this);
        }
    }

在TextView类里查找invalidateDrawable函数,去掉了 mDrawableRight 等代码

@Override
    public void invalidateDrawable(Drawable drawable) {
        if (verifyDrawable(drawable)) {
            final Rect dirty = drawable.getBounds();
            int scrollX = mScrollX;
            int scrollY = mScrollY;

            // IMPORTANT: The coordinates below are based on the coordinates computed
            // for each compound drawable in onDraw(). Make sure to update each section
            // accordingly.
            final TextView.Drawables drawables = mDrawables;
            if (drawables != null) {
                if (drawable == drawables.mDrawableLeft) {
                    final int compoundPaddingTop = getCompoundPaddingTop();
                    final int compoundPaddingBottom = getCompoundPaddingBottom();
                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;

                    scrollX += mPaddingLeft;
                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
                } 
                ········
                ········
            }

            invalidate(dirty.left + scrollX, dirty.top + scrollY,
                    dirty.right + scrollX, dirty.bottom + scrollY);
        }
    }

invalidate函数不陌生吧,一部分客户区域将被重新绘制。跟到这里,就明白问题出在哪里了,因为动画是要不断的重新绘制的,但是我自定义的类里没有重写invalidateDrawable函数,导致调用了父类TextView里的 invalidateDrawable。重新绘制的区域错了,所以点击按钮后,图片不旋转。

自定义类重写的函数是两个onDraw(Canvas canvas) 和 invalidateDrawable(Drawable drawable)

自定义类文件DrawableCenterButton.java

public class DrawableCenterButton extends TextView{

    public DrawableCenterButton(Context context) {
        super(context);
    }

    public DrawableCenterButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public DrawableCenterButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    int textWidth;
    int textHeight;
    Drawable mDrawableLeft = null;
    int startDrawableX = 0;
    int startDrawableY= 0;
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        initText();
    }

    private void initText() {
        String textStr = super.getText().toString();
        Rect rect = new Rect();
        getPaint().getTextBounds(textStr,0,textStr.length(),rect);
        getPaint().setColor(getTextColors().getDefaultColor());
        getPaint().setTextSize(getTextSize());
        textWidth = rect.width();
        textHeight = rect.height();

    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mDrawableLeft == null) mDrawableLeft = getCompoundDrawables()[0];

        if (mDrawableLeft == null) {
            super.onDraw(canvas);
            return;
        }
        int drawablePadding = getCompoundDrawablePadding();
        int drawableWidth = this.mDrawableLeft.getIntrinsicWidth();
        int drawableHeight = this.mDrawableLeft.getIntrinsicHeight();
        startDrawableX = (getWidth() >> 1) - ((drawablePadding + textWidth + drawableWidth) >> 1);
        startDrawableY = (getHeight() >> 1) - (drawableHeight >> 1);
        //画旋转图片
        canvas.save();
        canvas.translate(startDrawableX, startDrawableY);
        this.mDrawableLeft.draw(canvas);
        canvas.restore();

        //画文字
        int boxht = this.getMeasuredHeight() - this.getExtendedPaddingTop() - this.getExtendedPaddingBottom();
        int textht = getLayout().getHeight();
        int  voffsetText = boxht - textht >> 1;
        canvas.save();
        canvas.translate((float) (startDrawableX + drawableWidth + drawablePadding), (float) (getExtendedPaddingTop() + voffsetText));
        getLayout().draw(canvas);
        canvas.restore();
    }
    
    @Override
    public void invalidateDrawable(Drawable drawable) {
//        super.invalidateDrawable(drawable);
        final Rect dirty = drawable.getBounds();
        int scrollX = 0;
        int scrollY = 0;
        if(drawable == this.mDrawableLeft){
            scrollX = startDrawableX;
            scrollY = startDrawableY;
        }
        this.invalidate(dirty.left + scrollX-2, dirty.top + scrollY-2, dirty.right + scrollX+2, dirty.bottom + scrollY+2);
    }

    public Drawable getDrawableLeft() {
        return mDrawableLeft;
    }
}


成功!!!!


源码


参考:

关于TextView 宽度过大导致Drawable无法居中问题

http://blog.csdn.net/freesonhp/article/details/32695163

自定义控件让TextView的drawableLeft与文本一起居中显示

http://www.cnblogs.com/over140/p/3464348.html

View编程(3): invalidate()源码分析


你可能感兴趣的:(居中显示并旋转 android Button 里的属性drawableLeft)