English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
В Интернете нашёл компонент iOS PendulumView, который реализует анимацию маятника. Поскольку стандартные индикаторы прогресса действительно не очень привлекательны, хочу предложить создать пользовательский View для достижения такого эффекта, который можно будет использовать в индикаторах прогресса загрузки страницы.
Не будем говорить лишнее, сначала посмотрим效果图
Черная нижняя кайма - это неаккуратное занесение при записи, её можно пропустить.
Поскольку это пользовательский View, мы следуем стандартному процессу:第一步 - определить пользовательские свойства
Свойства, определяемые пользователем
Создание файла свойств
В Android-проекте создайте файл attrs.xml в директории res->values, содержимое файла такое:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="PendulumView"> <attr name="globeNum" format="integer"/> <attr name="globeColor" format="color"/> <attr name="globeRadius" format="dimension"/> <attr name="swingRadius" format="dimension"/> </declare-styleable> </resources>
Свойство name в declare-styleable используется для ссылки на этот файл свойств в коде. В большинстве случаев name указывает на имя нашего пользовательского класса View, что является интуитивно понятным.
Использование styleale позволяет системе выполнить много операций (массивы int[], константы индексов) и упростить наш процесс разработки, например, в приведенном ниже коде используется R.styleable.PendulumView_golbeNum, которые автоматически генерируются системой.
Свойство globeNum означает количество шаров, globeColor означает цвет шаров, globeRadius означает半径 шара, swingRadius означает半径 колебания
Чтение значений свойств
В конструкторе пользовательского view через TypedArray читаются значения свойств
Through AttributeSet можно получить значения свойств, но если значение свойства является типом ссылке, то получаем только ID, и необходимо продолжить через парсинг ID для получения真正的 значения свойства, а TypedArray напрямую помогает нам выполнить это.
public PendulumView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //Использование TypedArray для чтения пользовательских значений свойств TypedArray ta = context.getResources().obtainAttributes(attrs, R.styleable.PendulumView); int count = ta.getIndexCount(); for (int i = 0; i < count; i++) { int attr = ta.getIndex(i); switch (attr) { case R.styleable.PendulumView_globeNum: mGlobeNum = ta.getInt(attr, 5); break; case R.styleable.PendulumView_globeRadius: mGlobeRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics())); break; case R.styleable.PendulumView_globeColor: mGlobeColor = ta.getColor(attr, Color.BLUE); break; case R.styleable.PendulumView_swingRadius: mSwingRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics())); break; } } ta.recycle(); //избегать проблем при следующем чтении mPaint = new Paint(); mPaint.setColor(mGlobeColor); }
переписать метод OnMeasure()
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //高度为小球半径+摆动半径 int height = mGlobeRadius + mSwingRadius; //宽度为2*摆动半径+(小球数量-1)*小球直径 int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius; //如果测量模式为EXACTLY,则直接使用推荐值,如不为EXACTLY(一般处理wrap_content情况),使用自己计算的宽高 setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height); }
其中
int height = mGlobeRadius + mSwingRadius;
<pre name="code" class="java">int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
用于处理测量模式为AT_MOST的情况,一般是自定义View的宽高设置为了wrap_content,此时通过小球的数量,半径,摆动的半径等计算View的宽高,如下图:
以小球个数5为例,View的大小为下图红色矩形区域
重写onDraw()方法
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制除左右两个小球外的其他小球 for (int i = 0; i < mGlobeNum - 2; i++) { canvas.drawCircle(mSwingRadius + (i + 1) * 2 * mGlobeRadius, mSwingRadius, mGlobeRadius, mPaint); } if (mLeftPoint == null || mRightPoint == null) { //初始化最左右两小球坐标 mLeftPoint = new Point(mSwingRadius, mSwingRadius); mRightPoint = new Point(mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1), mSwingRadius); //Запуск анимации колебания startPendulumAnimation(); } //Рисование левых и правых шариков canvas.drawCircle(mLeftPoint.x, mLeftPoint.y, mGlobeRadius, mPaint); canvas.drawCircle(mRightPoint.x, mRightPoint.y, mGlobeRadius, mPaint); }
Метод onDraw() является ключом к созданию пользовательского View, в этом методе рисуется видимость View. Код сначала рисует все шарики, кроме крайних левых и правых шариков, затем проверяет координаты левых и правых шариков. Если это первый раз, то координаты пусты, инициализируются координаты двух шариков и запускается анимация. В конце через значения x, y mLeftPoint и mRightPoint рисуются два шарика.
mLeftPoint и mRightPoint являются объектами android.graphics.Point, они используются только для хранения координат x, y левых и правых шариков.
Использование анимации свойств
public void startPendulumAnimation() { //Использование анимации свойств final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //参数fraction用于表示动画的完成度,我们根据它来计算当前的动画值 double angle = Math.toRadians(90 * fraction); int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle)); int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle)); Point point = new Point(x, y); return point; } }, new Point(), new Point()); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Point point = (Point) animation.getAnimatedValue(); //Получение текущего значения fraction float fraction = anim.getAnimatedFraction(); //Проверяем, уменьшался ли fraction, а затем увеличился, то есть проверяем, находится ли шар в состоянии подъема //Переключаем шар перед каждым поднятием if (lastSlope && fraction > mLastFraction) { isNext = !isNext; } //Через постоянное изменение координат x, y левых и правых шариков достигается анимационный эффект //Использование isNext для определения, должен ли двигаться левый шарик или правый шарик if (isNext) { //Когда левый шарик колеблется, правый шарик находится в начальной позиции mRightPoint.x = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1); mRightPoint.y = mSwingRadius; mLeftPoint.x = mSwingRadius - point.x; mLeftPoint.y = mGlobeRadius + point.y; } else { //当右边小球摆动时,左边小球置于初始位置 mLeftPoint.x = mSwingRadius; mRightPoint.y = mSwingRadius; mRightPoint.x = mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + point.x; mRightPoint.y = mGlobeRadius + point.y; } invalidate(); lastSlope = fraction < mLastFraction; mLastFraction = fraction; } }); //设置永久循环播放 anim.setRepeatCount(ValueAnimator.INFINITE); //设置循环模式为倒序播放 anim.setRepeatMode(ValueAnimator.REVERSE); anim.setDuration(200); //Настройка interpolатора, контроль скорости изменения анимации anim.setInterpolator(new DecelerateInterpolator()); anim.start(); }
其中使用ValueAnimator.ofObject方法是为了可以对Point对象进行操作,更为形象具体。还有就是通过ofObject方法使用了自定义的TypeEvaluator对象,由此得到了fraction值,该值是一个从0-1变化的小数。所以该方法的后两个参数startValue(new Point()),endValue(new Point())并没有实际意义,也可以直接不写,此处写上主要是为了便于理解。同样道理也可以直接使用ValueAnimator.ofFloat(0f, 1f)方法获取到一个从0-1变化的小数。
final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //参数fraction用于表示动画的完成度,我们根据它来计算当前的动画值 double angle = Math.toRadians(90 * fraction); int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle)); int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle)); Point point = new Point(x, y); return point; } }, new Point(), new Point());
Через fraction мы вычисляем значение изменения угла движения шара, от 0 до 90 градусов
Значение mSwingRadius - mGlobeRadius представляет собой длину зеленой линии на рисунке, линия движения, линия центра движения шара - это дуга с半径ом (mSwingRadius - mGlobeRadius), изменяющийся значение X - (mSwingRadius - mGlobeRadius) * sin(angle), изменяющееся значение Y - (mSwingRadius - mGlobeRadius) * cos(angle)
Координаты центра движения соответствующего шара (mSwingRadius - x, mGlobeRadius + y)
Реальные координаты центра движения правого шара (mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + x, mGlobeRadius + y)
Угол координаты вертикали слева и справа одинаковый, а координата горизонтали различается.
float fraction = anim.getAnimatedFraction(); //Проверяем, уменьшался ли fraction, а затем увеличился, то есть проверяем, находится ли шар в состоянии подъема //Переключаем шар перед каждым поднятием if (lastSlope && fraction > mLastFraction) { isNext = !isNext; } //Записываем, уменьшалась ли lastFraction lastSlope = fraction < mLastFraction; //Записываем fraction из предыдущего раза mLastFraction = fraction;
Эти два кода используются для расчета времени переключения движения шара, этот анимационный ролик настроен на повторное воспроизведение, и режим повторения установлен на обратный порядок воспроизведения, поэтому один цикл анимации представляет собой процесс, когда шар поднимается и падает. В этом процессе значение fraction сначала равно 0, затем становится 1, а затем снова становится 0. Когда начинается новый цикл анимации? Это происходит в момент, когда шар готов к подниманию, и в этот момент можно переключить движение шара, чтобы实现 эффект анимации, когда шар слева падает, а шар справа поднимается, и когда шар справа падает, шар слева поднимается.
Так как же поймать этот момент времени?
В момент подбрасывания шара значение fraction неуклонно увеличивается, а в момент падения значение fraction неуклонно уменьшается. В момент, когда шар готов к подбрасыванию, это момент, когда fraction переходит от неуклонного уменьшения к неуклонному увеличению. В коде регистрируется, было ли значение fraction неуклонно уменьшаться в последний раз, и сравнивается с текущим значением fraction, если оба условия выполняются, то переключается на движение шара.
anim.setDuration(200); //Настройка interpolатора, контроль скорости изменения анимации anim.setInterpolator(new DecelerateInterpolator()); anim.start();
Настройка продолжительности анимации в 200 миллисекунд, читатель может изменить это значение, чтобы изменить скорость колебания шара.
Настройка анимации interpolатора, так как подбрасывание шара - это процесс постепенного замедления, а падение - это процесс постепенного ускорения, поэтому используется DecelerateInterpolator для реализации эффекта замедления, а при обратном воспроизведении - эффект ускорения.
Анимация запуска, пользовательский интерфейс для прогресс-бара с эффектом маятника реализован! Поторопитесь запустить и看看效果吧!
Вот и все, что было в этой статье, надеюсь, это поможет вам в изучении. Также希望大家多多支持呐喊教程。
Заявление: содержимое статьи взято из Интернета, авторские права принадлежат соответствующему владельцу. Контент предоставлен пользователями Интернета, загружен самостоятельно, сайт не имеет права собственности, не был обработан вручную, и не несет ответственности за соответствующие юридические вопросы. Если вы обнаружите подозрительное содержание, пожалуйста, отправьте письмо по адресу: notice#oldtoolbag.com (во время отправки письма замените # на @) для сообщения о нарушении и предоставьте соответствующие доказательства. Если факт будет подтвержден, сайт немедленно удаляет подозрительное содержимое.