Как нарисовать круг с радиальным градиентом в холсте?

Я создал кнопку круга, которая может изменить свой цвет при вызове функции. Я хочу создать еще один, который создает одну и ту же кнопку круга, но с радиальным градиентом, который начинается посередине с выбранным цветом и становится прозрачным, когда вы выходите из круга.

Я создал аналогичный код, используя тот, который был отправлен в Как установить стиль градиента для рисования объекта?, но не работает.

Код, который я пробовал, для этой породы:

mPaint.setShader(new RadialGradient(0, 0, height/3, Color.BLACK, Color.TRANSPARENT, Shader.TileMode.MIRROR));

Следующий класс - тот, который я создал для кнопки круга.

public class ColorGradientCircleButton extends View{

private Paint mPaint;
private Paint   mBitmapPaint;
private Bitmap  mBitmap;
private Canvas  mCanvas;
private int width, height;

public ColorGradientCircleButton(Context context) {
    super(context);
    init();
}
public ColorGradientCircleButton(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}
public ColorGradientCircleButton(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init();
}
private void init() {
    mPaint = new Paint();
    mPaint.setColor(Color.BLACK);
    mPaint.setStrokeWidth(1);
    mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mBitmapPaint = new Paint(Paint.DITHER_FLAG);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    width = w;
    height = h;
    mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    mCanvas = new Canvas(mBitmap);
    mCanvas.drawCircle(w/2, h/2, h/3, mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
}
public void changeColor(int color){
    mPaint.setColor(color);
    mCanvas.drawCircle(width/2, height/2, height/3, mPaint);
    invalidate();
}
}

Ответ 1

Мы должны перенести это в поля ответа.

OP в основном получил его здесь, и фактически обновленный OP gist блистателен.

Некоторые общие советы относительно первой попытки в вопросе:

1) В protected void onSizeChanged(int w, int h, int oldw, int oldh):

  • width = w; нет причин, по которым вы не можете позвонить getWidth(), когда это потребуется. Причина, по которой это целесообразно, состоит в том, что внутренняя ширина View устанавливается довольно поздно после onMeasure. Следовательно, onDraw может быть в следующий раз, когда вы захотите получить самую последнюю версию, поэтому используйте getter там.
  • mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);. Создание растрового изображения - это дорогостоящая операция с интенсивной памятью. Если вы не хотите писать растровое изображение в файл или отправить его в BitmapDrawable для ImageView или что-то еще, вам не нужно это делать. Особенно с эффектами, нарисованными на пользовательском интерфейсе с помощью библиотеки android graphics.
  • mCanvas = new Canvas(mBitmap);, за которым следует операция рисования на новом холсте. Это никогда не нужно. И все же я видел это (не работает) во многих кодовых базах и попытках. Я думаю, что это ошибка старой записи, которая заставила людей делать это, чтобы они могли трансформировать холст на пользовательский вид, не делая рисунок на остальной части холста. Кстати, если вам это нужно, используйте .restore() и .save(). Если вы видите new Canvas, быть подозрительным.

2) onDraw(...):

  • Да, вам нужно избегать действий в onDraw, например, создания объектов или любой тяжелой обработки. Но вам все равно нужно делать вещи в onDraw, которые вам нужно сделать в onDraw!
  • Итак, здесь вам просто нужно вызвать: canvas.drawCircle(float cx, float cy, float radius, Paint paint) с аргументами в соответствии с документами.
  • Это действительно не грех для onDraw. Если вы беспокоитесь о том, чтобы называть это слишком много, как может быть, если вся ваша кнопка анимации по экрану, вам нужно использовать аппаратное ускорение, доступное в более позднем интерфейсе API версии, как будет описано в статье под названием Оптимизация представления; очень полезное чтение, если вы используете множество пользовательских обращений.

3) Этот надоедливый радиальный градиент. Следующий вопрос, который у вас был, - это то, что вы правильно создали свою краску в методе init, чтобы создание объекта не было выполнено. Но тогда, вполне справедливо, у вас будет IllegalArgumentException ed (я думаю), потому что на этом этапе getHeight() для представления было 0. Вы пытались передать небольшие значения пикселей - это не сработает, если вы не узнаете какую-либо магию о размеры экрана.

Это не ваша проблема, как раздражающий цикл просмотра в основе дизайна Android. Исправить хотя и достаточно просто: просто используйте более позднюю часть процесса рисования вида после вызова onMeasure, чтобы установить фильтр краски.

Но есть некоторые проблемы с получением этого права, а именно, что иногда, досадно, onDraw вызывается до того момента, когда вы ожидаете его. В результате ваша краска будет нулевой, и вы не получите желаемого поведения.

Я нашел более надежное решение - просто сделать нахальную и озорную маленькую нулевую проверку в onDraw, а затем только построить там объект рисования. Это не строго говоря, оптимально, но с учетом сложного способа, с помощью которого объекты Paint соединяются с родным слоем на основе Android, лучше, чем пытаться расположить конфигурацию и конструкцию краски во многих часто называемых местах. И это делает для более четкого кода.

Это будет выглядеть (внося изменения в суть):

        @Override
        protected void onDraw(final Canvas canvas) {
            super.onDraw(canvas);
            if (mPaint == null) {
                mPaint = new Paint();
                mPaint.setColor(Color.BLACK);
                mPaint.setStrokeWidth(1);
                mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
                mPaint.setShader(new RadialGradient(getWidth() / 2, getHeight() / 2,
                        getHeight() / 3, Color.TRANSPARENT, Color.BLACK, TileMode.MIRROR));
            }
            width = getWidth();
            height = getHeight();
            canvas.drawCircle(width / 2, height / 2, height / 3, mPaint);
        }

Итак, обратите внимание на несколько изменений - я думаю, что из вашего описания вы хотите, чтобы два цвета обменивались аргументами, также не забывайте центрировать центр вашего градиента в вашем представлении: аргументы width/2 и height/2.

Удачи!