Android Custom Views

custom-views-cover

Android platformuna geliştirmeler yaparken varsayılan olarak gelen view’lar (Button, TextView vb.) her ne kadar işimizi görse de bazen öyle bir an geliyor ki hiç var olmamış bir view komponentine ihtiyacımız oluyor. Böyle bir durumla karşılaştığımızda hayalimizdeki view’ımızı kendimiz oluşturmamız gerekiyor. Bu yazıda kendi view’larımızı nasıl oluşturabiliceğimiz hakkında bilgiler vermeye çalışacağım.

Not: Anlaşılabilirliği artırmak için “view” kelimesinin türkçe karşılığı olan “görünüm” demeyeceğim. Bu sebepten dolayı “viewlar” “view’ın” gibi kelimelere  takılmamanızı rica ediyorum.

Custom view oluşturabilmemiz için ihtiyaç duyduğumuz en önemli şey tabikide View Sınıfı 🙂 Custom view’larımızı View sınıfından kalıtım alarak oluşturacağız.

Viewların renderı CPU kullanılarak yapılır. Bu yüzden gereksiz işleme yer vermeden viewlarımızı oluşturacak sınıflar tasarlamalıyız. Tasarladığımız view’ın XML yardımıyla özelleştirilebilir ve erişilebilirliğinin yüksek olması gerekmektedir.

Çizim işlemlerini anlamak için öncelikle dikdörtgen, daire, çember, çizgi, yay ve yol çizelim.

BasicShapesView.java (Yorum satırlarına dikkat !)

package com.kodcu.customviewexample;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by gokselpirnal on 22/08/16.
 */
public class BasicShapesView extends View {

    private Paint rectangleP, discP, ringP, lineP, arcP, pathP;

    public BasicShapesView(Context context) {
        super(context);
        init();
    }

    public BasicShapesView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public BasicShapesView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        rectangleP = new Paint();
        rectangleP.setAntiAlias(true);      // pixelleri saymak istemiyoruz
        rectangleP.setColor(Color.parseColor("#95a5a6"));     // gri renk olacak
        rectangleP.setStrokeWidth(10);      // kenar çizgisi genişliği
        // 10 verdik ama bu statik bi değer dinamikleştirmek daha kullanışlı olacaktır
        // aşağıdaki örneklerde nasıl dinamikleştirildiğine ulaşabilirsiniz 
        rectangleP.setStyle(Paint.Style.STROKE); // içi boş olacak

        discP = new Paint(Paint.ANTI_ALIAS_FLAG); // kurucudada belirtebiliriz
        discP.setStyle(Paint.Style.FILL); // içi dolu olacak ve bu yüzden stroke width vermemize gerek yok

        ringP = new Paint(Paint.ANTI_ALIAS_FLAG);
        ringP.setColor(Color.parseColor("#f39c12"));
        ringP.setAlpha(150); // şeffaflık 0-255 arası. Rengi set ettikten sonra alpha kullanılmalı
        ringP.setStrokeWidth(30);
        ringP.setStyle(Paint.Style.STROKE);

        lineP = new Paint(Paint.ANTI_ALIAS_FLAG);
        lineP.setColor(Color.parseColor("#27ae60"));
        lineP.setShadowLayer(10.0f, 0.0f, 2.0f, Color.BLACK); // gölgesi olacak
        lineP.setStrokeWidth(20);
        lineP.setStyle(Paint.Style.STROKE);

        setLayerType(LAYER_TYPE_SOFTWARE, null); // gölgeleri görebilmemiz yazılım katmanını aktifleştirmemiz gerekiyor
        // fakat tavsiye edilmez çünkü donanımsal işlemi iptal eder
        // yazılımsal olarak halletmeye çalıştığından yavaşlık söz konusu olur

        arcP = new Paint(Paint.ANTI_ALIAS_FLAG);
        arcP.setColor(Color.parseColor("#1abc9c"));
        arcP.setStrokeWidth(50);
        arcP.setStyle(Paint.Style.STROKE);

        pathP = new Paint(Paint.ANTI_ALIAS_FLAG);
        pathP.setColor(Color.parseColor("#e74c3c"));
        pathP.setStrokeWidth(20);
        pathP.setStyle(Paint.Style.FILL);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension((widthMeasureSpec), (widthMeasureSpec)); // viewımız kare olacak
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int x = getMeasuredWidth();

        // boyut bilgisine ihtiyacımız olduğu için burada erişiyoruz
        Shader shader = new RadialGradient(x / 2, x / 2, x / 2, Color.parseColor("#16a085"), Color.parseColor("#2c3e50"), Shader.TileMode.MIRROR);
        discP.setShader(shader); // renk geçişi olacak

        // hangi şekil sonra basılırsa üstte o olur
        canvas.drawCircle(x / 2, x / 2, x / 2, discP);
        canvas.drawRect(x / 4, x / 4, x - x / 4, x - x / 4, rectangleP);
        canvas.drawCircle(x / 4, x / 2, x / 3, ringP);
        canvas.drawLine(x, x, x / 2, x / 2, lineP);
        canvas.drawArc(new RectF(0, 0, x - x / 4, x - x / 4), 45, 90, false, arcP); // 45 dereceden başlayıp 90 derece tara

        Path path = new Path();
        path.moveTo(x / 2, x / 2);
        path.lineTo(x / 2 - x / 3, x / 2 - x / 3);
        path.lineTo(x / 2 - x / 3, x / 2);
        path.arcTo(new RectF(x / 2 - x / 3, x / 2, x / 2, x), 180, 180);
        path.arcTo(new RectF(x / 2, x / 2, x / 2 + x / 3, x), 180, 180);
        path.lineTo(x / 2 + x / 3, x / 2 - x / 3);
        path.close();
        canvas.drawPath(path, pathP);

    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.kodcu.customviewexample.BasicShapesView
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

Telefon Çıktısı (1080 x 1920):

screen1

Tablet Çıktısı(1280*800):

screen2

onDraw metodunda gördüğünüz gibi şekillerimizi çizen metodlar (drawXXX) 4 işleme tabi tutulmuş parametreler almakta. Bu parametreler canvas içindeki şekillerin konumu ve boyutu gibi değerleri ifade eder.

Paint nesnelerimizi belirtirken Stroke Width değerini statik olarak vermiştik bu iki çıktıya bakarak statik değer verdiğimizde oluşan orantısızlığı görebilirsiniz.(Tabletin çözünürlüğü daha düşük olduğu için çizgiler daha kalın görünüyor.)

Yani burada dikkat etmemiz gereken önemli nokta : Ekran Çözünürlüğü

Ekran Çözünürlüğü

Android Cihazların Bazı Ekran Çözünürlükleri

800*400

2560*1600

1366*768

1280*800

1280*768

1024*768

1024*600

960*640

960*540

854*480

800*600

800*480

Listede de gördüğünüz gibi sabit bir çözünürlük yok. Eğer biz viewımız içindeki çizimleri sabit değer vererek yaparsak test ettiğiniz cihaz çözünürlüğü haricinde görünüm orantısız olacaktır. Bu yüzden yukarıda da gördüğünüz gibi view’ımızın genişliğini referans alarak konumlandırma ve boyutlandırma yaptık. Bu sayede hangi çözünürlük olursa olsun view’ımız aynı orantıda görünecektir.

XML ile özelleştirme

Temel çizim adımlarını öğrendiğimize göre artık viewlarımızı xml ile özelleştirilebilir hale getirmeyi öğrenelim.

Amacımız aşağıdaki görseldeki view’ı gerçeklemek olsun

screen3

 ilk adımımız res > values klasörünün altına attr.xml dosyası oluşturmak ve CircularProgressBarView için özelleştirilebilir özellikler(attribute) tanımlamak.

Bu view için XML ile özelleştirilebilir özelliklerimiz şunlar olmasını istiyorum:

  • Kenarlık rengi
  • Pasif olan iç kısım rengi
  • Aktif olan iç kısım rengi
  • Aktif yüzde

bu isteklere karşılık attr.xml dosyasına aşağıdaki hale getiriyorum.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircularProgressBarView">
        <attr name="cpb_borderColor" format="color" />
        <attr name="cpb_innerColor" format="color" />
        <attr name="cpb_percentColor" format="color" />
        <attr name="cpb_percent" format="integer" />
    </declare-styleable>
</resources>

CircularProgressBarView :

package com.kodcu.customviewexample;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by gokselpirnal on 23/08/16.
 */
public class CircularProgressBarView extends View {

    private Paint borderPaint, innerPaint, percentPaint;
    private int borderColor, innerColor, percentColor;
    private int percent;

    public CircularProgressBarView(Context context) {
        super(context);
        init();
    }

    public CircularProgressBarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // layout içinde xml ile belirtilecek değerlere erişiyoruz
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircularProgressBarView);
        percentColor = typedArray.getColor(R.styleable.CircularProgressBarView_cpb_percentColor, 0xff16a085); // layoutta değer atanmamışsa default olarak 0xff16a085 kullan diyoruz
        borderColor = typedArray.getColor(R.styleable.CircularProgressBarView_cpb_borderColor, 0xff2c3e50);
        innerColor = typedArray.getColor(R.styleable.CircularProgressBarView_cpb_innerColor, 0xff34495e);
        percent = typedArray.getInt(R.styleable.CircularProgressBarView_cpb_percent, 0);
        if (this.percent < 0) percent = 0;
        else if (this.percent > 100) percent = 100;
        typedArray.recycle();
        init();
    }

    public CircularProgressBarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        borderPaint.setAntiAlias(true);
        borderPaint.setColor(borderColor);
        borderPaint.setStyle(Paint.Style.STROKE);

        innerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        innerPaint.setAntiAlias(true);
        innerPaint.setColor(innerColor);
        innerPaint.setStyle(Paint.Style.STROKE);

        percentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        percentPaint.setColor(percentColor);
        percentPaint.setStyle(Paint.Style.STROKE);
        percentPaint.setStrokeCap(Paint.Cap.ROUND); // yayımızın uçlarının oval olmasını sağlar
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension((widthMeasureSpec), (widthMeasureSpec)); // viewımız kare olsun
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int x = getMeasuredWidth();

        borderPaint.setStrokeWidth(x / 10);
        innerPaint.setStrokeWidth(x / 10 - x / 50);
        percentPaint.setStrokeWidth(x / 10 - x / 50);

        // hangi şekil sonra basılırsa üstte o olur
        // bu kuraldan faydalanarak borderPainti ve innerPainti üst üste bindirerek
        // artan kısmın kenarlık gibi görünmesini sağladık
        canvas.drawCircle(x / 2, x / 2, x / 2 - x / 20, borderPaint); // yarıçaptan x/20 çıkardık ki çember view alanından taşmasın
        canvas.drawCircle(x / 2, x / 2, x / 2 - x / 20, innerPaint); 
        canvas.drawArc(new RectF(x / 20, x / 20, x - x / 20, x - x / 20), 0, (360.0f / 100.0f) * percent, false, percentPaint); // 0 dereceden başlayıp girilen yüzde değerinin açısal karşılığını tara
    }
}

activity_main.xml :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <com.kodcu.customviewexample.CircularProgressBarView
        android:id="@+id/progress"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        app:cpb_borderColor="#2c3e50"
        app:cpb_innerColor="#34495e"
        app:cpb_percent="60"
        app:cpb_percentColor="#16a085" />

</LinearLayout>

Erişilebilirlik

Setter, getter, listener vb. gibi metodların varlığı ve fonksiyonalitesi  view’ımızın erişilebilirliğini yükseltir. Yüzde(percent) değerinin setter metodu ile erişilebilirliğe bir örnek gösterelim.

Göstermelik olacağı için sadece yüzde değerinin setter metodunu göstereceğim. Renk değişiklikleri de aynı şekilde setter metodlarıyla yapabilirsiniz. Setterlardaki atama işlemlerinden sonraki en önemli kısım invalidate(); metodunu çağırmak. Bu metod view’ı yeniden render eder ve değişiklikler görünür hale gelir.

Yukarıdaki custom view sınıfımıza şu satırları ekleyelim

public void setPercent(int percent) {
    if (this.getPercent() < 0) this.percent = 0;
    else if (this.getPercent() > 100) this.percent = 100;
    else this.percent = percent;
    invalidate();
}

Artık uygulamamız içinde dinamik olarak yüzdeyi şu şekilde değiştirebilirsiniz.

CircularProgressBarView circularProgressBarView = (CircularProgressBarView) findViewById(R.id.progress);
circularProgressBarView.setPercent(new Random().nextInt(100) + 1);

Animasyon

Animasyon yapabilmemiz için framelere ihtiyacımız olacak. Frameleri aşağıdaki metodu kullanarak kendimiz oluşturacağız. Her bir zaman dilimi bir frame olacak.

postInvalidateDelayed(1000 / 60); // 60 fps

Yapacağımız animasyon progress barımızın ilk yüklendiğinde 0’dan belirtilen yüzdeye kadar yavaşca dolması olsun. Aşağıdaki gifteki görebileceğiniz gibi.

progress-animation-gif

Bu animasyonu gerçekleştirmek çok basit, ihtiyacımız olan tek şey animasyonu sağlayacak bir adet değişken tanımlamak. Ben bu değişkene animationPercent adını verdim ve onDraw() metodunu şu şekilde değiştirdim. Kod bloğu sonundaki if kısmına ve drawArc() metodunun 3.parametresi yani taranacak açı değerine dikkat etmeniz gerekmekte

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int x = getMeasuredWidth();

    borderPaint.setStrokeWidth(x / 10);
    innerPaint.setStrokeWidth(x / 10 - x / 50);
    percentPaint.setStrokeWidth(x / 10 - x / 50);

    // hangi şekil sonra basılırsa üstte o olur
    // bu kuraldan faydalanarak borderPainti ve innerPainti üst üste bindirerek
    // artan kısmın kenarlık gibi görünmesini sağladık
    canvas.drawCircle(x / 2, x / 2, x / 2 - x / 20, borderPaint); // yarıçaptan x/20 çıkardık ki çember view alanından taşmasın
    canvas.drawCircle(x / 2, x / 2, x / 2 - x / 20, innerPaint); 
    canvas.drawArc(new RectF(x / 20, x / 20, x - x / 20, x - x / 20), 0, (360.0f / 100.0f) * animationPercent, false, percentPaint); // 0 dereceden başlayıp girilen yüzde değerinin açısal karşılığını tara

    if(animationPercent < percent){
        animationPercent++;
        this.postInvalidateDelayed(1000 / 60); // 60 fps
    }

}
 Bu yazı sayesinde Android platformunda kendi view’larınızı nasıl oluşturabileceğiniz hakkında fikir sahibi olmanıza yardımcı olduğunu umuyorum. Sorunlarınız için yorum kısmına not bırakabilirsiniz.
2 Comments
  • Posted at 11:18, 06/09/2016

    Göksel bey engin bilgileriniz ile bizi aydınlattığınız için teşekkürler, acaba glsl ya da hlsl kullanabilir miyim “view” larımda ?

  • Murat İnan
    Posted at 23:05, 29/04/2019

    Muhteşem. Teşekkürler!

Post a Comment

Comment
Name
Email
Website