居中显示并旋转 android Button 里的属性drawableLeft
如图,点击同步按钮,同步图片要旋转起来,直到同步完毕。有一个容易实现的方法,就叫“方法1”吧(下面会用的),一个LinearLayout里面包含一个ImageView和一个TextView并且居中显示,监听LinearLayout的点击事件,然后旋转ImageView。
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文件
模仿写试了试发现, 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"?>
然后
在Button的点击事件里处理旋转问题
public void onClick(View v) {Button btnButton = (Button) v;// 获取android:drawableLeftDrawable 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 ListView中自定义SimpleAdapter动态添加ratingBar及图片
- Android(安卓)TextView加载HTMl图文之添加点击事件和查看图片
- Android之Bitmap 高效加载
- Android开发之Memory类的分析
- Android(安卓)Camera中无法回调PictureCallback接口onPictureTak
- Android(安卓)4.2一些变动
- Android(安卓)Audio代码分析=Audio Strategy
- QuickSand图片点击后分裂成几份消失效果
- Android(安卓)高通4.4.4 源码 如何屏蔽Home键