Showing posts with label Android. Show all posts
Showing posts with label Android. Show all posts

Kenalan dengan Jetpack Compose : Cara modern membuat UI di aplikasi Android

Friday, July 03, 2026 Add Comment


Halo guys, kali ini kita akan membahas tentang Development Android UI dengan Jetpack Compose. Kalau pada artikel sebelum-sebelumnya, ketika membuat desain UI di android kita menggunakan XML. Pada masa sekarang ini, tidak perlu lagi bergantung pada XML karena Jetpack Compose telah menjadi standar utama dalam modern android development.

Jetpack Compose adalah Framework UI dari Google untuk menyederhanakan proses pengembangan UI di Android. Project baru saat ini banyak mengadopsi Compose sebagai fondasi utama karena menawarkan kode yang lebih ringkas, mudah dibaca, dan lebih mudah dirawat dibandingkan pendekatan berbasis XML.

XML vs Jetpack Compose 

Berikut adalah perbedaan antara XML dan Jetpack Compose :

  • UI & Logic
    • XML :  Didefinisikan langsung di dalam file XML. UI dan logic berada di dua tempat yang berbeda.
    • Jetpack Compose : Didefinisikan langsung menggunakan Kotlin. UI dan logic dalam satu barisan code sehingga lebih ringkas dan mudah dipahami. 
  • Development Approach
    • XML : XML menggunakan pendekatan Imperative, artinya developer memberi instruksi secara eksplisit mengenai apa yang harus diubah pada tampilan. Setiap perubahan harus memperbaharui komponen UI secara manual.
    • Jetpack Compose : Jetpack Compose menggunakan pendekatan Declarative, artinya developer hanya mendefinisikan seperti apa UI seharusnya. Selebihkan framework akan menangani otomatis update UI dengan mengikuti perubahan state. 
  • Boilerplate Code
    • XML : Lebih banyak karena harus menulis code tambahan seperti XML, View Binding, Fragment Transaction dan sinkronisasi antara XML dan Kotlin
    • Jetpack Compose : Lebih sedikit karena menghilangkan sebagian besar boilerplate dari XML.  
  • Preview 
    • XML :  Layout Preview terbatas dan harus build ulang aplikasi.
    • Jetpack Compose : Live Preview dan hot reload tanpa rebuild aplikasi.
  • Reusability
    • XML : Include Layout / Custom View
    • Jetpack Compose : Composable Function

Kesimpulannya, Jetpack Compose menawarkan pendekatan yang lebih modern dengan kode yang lebih ringkas, mudah dipahami, dan lebih sesuai dengan pengembangan Android saat ini.

Oke sekarang kita akan langsung praktekkan membuat aplikasi Android dengan Jetpack Compose.

Pertama-tama download dan install Android Studio dulu jika belum pernal terinstall. Bisa di download dan install  di sini https://developer.android.com/studio

Saya pakai Android Studio versi Quail 1. Untuk Kotlin menggunakan versi 2.4.0 dan AGP (Android Gradle Plugin) versi 9.2.1. Bisa dilihat di source code project yang akan saya share di akhir artikel.

1. Buat project baru di Android Studio, kemudian pilih Empty Activity


 2. Isikan nama project, package name, dll

 

3. Setelah selesai maka struktur projectnya akan seperti berikut.

  

 4. Kita coba jalankan projectnya di emulator dan hasilnya sebagai berikut.

5. Kita akan coba menambahkan komponen lain seperti Column, Row, Text dan Button dengan Composable Function 

Composable Function adalah fungsi Kotlin yang digunakan untuk mendeskripsikan tampilan (UI). Fungsi ini ditandai dengan anotasi @Composable.


@Composable
fun Greeting(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp)
        ) {
            Text(
                text = "Name : ",
            )
            Text(
                text = "Wim",
            )
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp)
        ) {
            Text(
                text = "Email : ",
            )
            Text(
                text = "wim.sonevel@yahoo.co.id",
            )
        }

        Button(
            onClick = {
                println("Button pressed")
            }
        ) {
            Text("Click Me")
        }
    }
}


Di function yang kita buat terdapat komponen Modifier. Modifier digunakan untuk mengatur ukuran, warna, padding, margin, posisi, background, click listener dan berbagai properti lainnya.
6. Sekarang kita akan update perubahan UI dengan Live Edit tanpa harus rebuild ulang di emulator. Edit function berikut dengan ditandai dengan anotasi @Preview.

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    AndroidComposeTheme {
        Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
            Greeting(modifier = Modifier.padding(innerPadding))
        }
    }
}


Untuk menjalankan Live Edit, klik icon refresh di pojok atas kanan emulator.

7. Berikut source code lengkap di MainActivity.


package com.example.android_compose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.android_compose.ui.theme.AndroidComposeTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AndroidComposeTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

@Composable
fun Greeting(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp)
        ) {
            Text(
                text = "Name : ",
            )
            Text(
                text = "Wim",
            )
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp)
        ) {
            Text(
                text = "Email : ",
            )
            Text(
                text = "wim.sonevel@yahoo.co.id",
            )
        }

        Button(
            onClick = {
                println("Button pressed")
            }
        ) {
            Text("Click Me")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    AndroidComposeTheme {
        Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
            Greeting(modifier = Modifier.padding(innerPadding))
        }
    }
}

8. Jalankan aplikasi dan hasilnya sebagai berikut.



Source code lengkapnya bisa dilihat di https://github.com/wimsonevel/Android-Compose

Oke sekian dulu artikel kali ini, next kita akan melanjutkan dengan membuat komponen lainnya dengan Jetpack Compose.

Semoga bermanfaat dan terima kasih sudah berkunjung. ^^

Dasar - dasar Pemrograman Kotlin untuk Pengembangan Aplikasi Android Modern

Tuesday, June 30, 2026 Add Comment

        
Halo Guys, saya kembali lagi setelah hampir 8 tahun vacuum menulis di blog. Saya bersyukur sampai hari ini diberikan kesehatan dan kesempatan lagi untuk mengamalkan ilmu yang sudah saya pelajari selama ini. Mudah-mudahan saya bisa konsisten lagi untuk memberikan edukasi ke teman-teman pembaca setia blog ini di tengah gempuran AI saat ini. Saya rasa teman-teman harus punya pengetahuan dasar-dasarnya agar tetap memiliki fundamental yang kuat ketimbang hanya mengandalkan AI saja tanpa tahu apa yang dibuat. Dari 8 tahun tentunya banyak sekali ilmu dan pengalaman yang sudah saya raih. Pada kesempatan kali ini, saya akan menulis lagi tentang Android Development setelah tertinggal jauh dari artikel sebelumnya dan sekarang banyak teknologi baru yang digunakan dalam mengembangkan aplikasi android. Oke langsung saja kali ini saya akan membahas tentang bahasa pemrograman kotlin yang saat ini menjadi teknologi utama di dunia Android Development dari yang sebelumnya menggunakan Java.

Kenapa Kotlin?
Kotlin adalah bahasa pemrograman modern untuk mengembangkan aplikasi jadi lebih mudah dan lebih produktif. Bahasa kotlin dirancang agar lebih ringkas, efisien, mudah dibaca, dan membantu mengurangi kesalahan yang sering terjadi saat menulis kode.
Pada tahun 2017, Google secara resmi mengumumkan kotlin sebagai bahasa resmi dalam pengembangan Android. Sejak saat itu, kotlin mendapat dukungan dari Google dan semakin banyak digunakan oleh developer di seluruh dunia.

Keunggulan Kotlin
Berikut ini adalah beberapa keunggulan kotlin :

1. Sintaks Lebih Ringkas
        Penulisan kode lebih singkat dibanding Java dan minim kode berulang (boilerplate).
2. Null Safety
        Fitur null safety membantu mencegah kesalahan akibat null, penyebab paling umum terjadi crash  pada aplikasi android.
3. Interoperabilitas dengan Java
        Kotlin dapat digunakan bersama Java dalam satu project termasuk memanfaatkan library Java yang sudah ada.
4. Resmi Didukung untuk Android
        Kotlin merupakan bahasa yang direkomendasikan oleh Google untuk pengembangan aplikasi Android
5. Coroutines
        Kotlin menyediakan fitur Coroutines untuk menjalankan proses asinkron dengan lebih mudah dan efisien.
6. Multiplatform
        Selain Android, Kotlin juga dapat digunakan untuk membangun aplikasi backend, web, ios dan desktop dalam Kotlin Multiplatform.
7. Dukungan Komunitas dan Ekosistem yang Berkembangan
        Dukungan dan popularitas kotlin terus meningkat sehingga dokumentasi, library, serta komunitas developer-nya semakin lengkap.

Kotlin Data Types, Variables and Operators

Data Type di kotlin terdiri dari :


Int, Double, Long, Float, Boolean, Char, String

Contohnya :

val age: Int = 30
val price: Double = 99.99
val isActive: Boolean = true
val grade: Char = 'A'
val name: String = "Wim"

Conditions and Booleans 

Kotlin memiliki Boolean dan Operator Boolean seperti :


<, ==, >, !=, <=, >=

Contohnya :

val isLoggedIn = true

val age = 25

println(age >= 18)
println(age < 18)

Untuk Condition sendiri terdiri dari :
- If Expression

val age = 25

if (age >= 18) {
    println("You are an adult")
}

- If Else Expression

val age = 15

if (age >= 18) {
    println("You are an adult")
} else {
    println("You are underage")
}

- Else If Expression

val score = 85

if (score >= 90) {
    println("Grade A")
} else if (score >= 80) {
    println("Grade B")
} else if (score >= 70) {
    println("Grade C")
} else {
    println("Grade D")
}

- If as an Expression
Salah satu keunikan di kotlin adalah if di kotlin dapat digunakan sebagai sebuah expression yang langsung mengembalikan sebuah nilai.

val age = 20

val status = if (age >= 18) {
    "Adult"
} else {
    "Underage"
}

println(status)

Fitur lainnya dari kotlin adalah membuat pengecekan rentang nilai menjadi lebih sederhana dalam Range operator (...). Kita akan coba menggunakan range dalam statement If.

val score = 85

if (score in 75..100) {
    println("Passed")
}

- When Statement
Secara definisi hampir sama dengan switch, tetapi jauh lebih fleksibel. Berikut contohnya :


val day = 1

when (day) {
    1 -> println("Monday")
    2 -> println("Tuesday")
    3 -> println("Wednesday")
    4 -> println("Thursday")
    5 -> println("Friday")
    else -> println("Weekend")
}


Arrays and Lists 

- Array adalah kumpulan data yang bersifat fixed size. Artinya tidak dapat ditambah atau dikurangi elemennya, kecuali di copy ke array baru.


val fruits = arrayOf("Apple", "Strawberry", "Watermelon")

Yang unik di kotlin adalah array tidak terikat pada tipe data element. Artinya kita bisa menggabungkan beberapa tipe data yang berbeda.

val mix = arrayOf("Apple", 20)

- List adalah kumpulan data yang lebih fleksibel dibandingkan array. List merupakan bagian dari Kotlin Collections yang mana kita bisa memanfaatkan fitur-fitur di dalamnya untuk mengolah data seperti filter(), map(), sorted(), dan find().
Ada dua jenis List di kotlin :
  • List (Immutable)
  • MutableList (Mutable)

Immutable List
Karena bersifat immutable, data hanya dapat dibaca dan tidak dapat diubah setelah dibuat.

val fruits = listOf("Apple", "Strawberry", "Watermelon")

println(fruits)

MutableList List
Kebalikannya dari Immutable List, data dapat ditambah, dihapus dan diubah.

val fruits = mutableListOf("Apple", "Strawberry", "Watermelon")
fruits.add("Orange")
fruits.remove("Watermelon")
fruits[0] = "Mango"

println(fruits)

Loops 

Looping atau perulangan dapat dilakukan di Array maupun di List


val fruits = listOf("Apple", "Strawberry", "Watermelon")

for (fruit in fruits) {
    println(fruit)
}

Output:

Apple
Strawberry
Watermelon

Jika ingin mengakses index dan value

val fruits = listOf("Apple", "Strawberry", "Watermelon")

for ((index, fruit) in fruits.withIndex()) {
	println("Item at $index is $fruit\n")
}

Output:

Item at 0 is Apple
Item at 1 is Strawberry
Item at 2 is Watermelon

Selain perulangan maju, di kotlin juga bisa looping mundur, step dan alphabet.

for (i in 1..5) print(i)

Output : 12345

for (i in 5 downTo 1) print(i)

Output : 54321

for (i in 3..6 step 2) print(i)

Output : 35

for (i in 'a'..'f') print (i)

Output : abcdef


Nullable and non-nullable variables 

Di kotlin ada fitur Null Safety yang dirancang untuk mencegah kesalahan oleh nilai null, yang sering menyebabkan aplikasi crash terutama NullPointerException. Secara default, semua variabel di Kotlin bersifat non-nullable, artinya variabel tersebut tidak boleh menyimpan nilai null kecuali harus dideklarasikan secara eksplisit sebagai nullable.

Non-Nullable Variables
Variabel non-nullable hanya dapat menyimpan nilai sesuai dengan tipe datanya.


val name: String = "Wim"

Nullable Variables
Untuk mendeklarasikan sebuah variabel menyimpan nilai null, tambahkan tanda (?) setelah tipe data.

var name: String? = null

Safe Call Operator
Di nullable variable, kita tidak dapat langsung mengakses properti atau fungsi miliknya karena nilai bisa saja null. Maka dari itu gunakan safe call operator (?.) untuk mengaksesnya.

var name: String? = null
println(name?.length)

Elvis Operator
Operator ini berfungsi memberikan nilai default apabila sebuah variabel bernilai null dengan oerator (?:).

var name: String? = null
val displayName = name ?: "No Name"

println(displayName)

Not-Null Assertion
Operator Not-Null Assertion (!!) berfungsi untuk mengubah nullable menjadi non-nullable.

val name: String? = "Wim"

println(name!!.length)

Apabila ternyata nilainya null, maka akan mengalami error NullPointerException. Oleh karena itu, penggunaan (!!) sebaiknya dihindari kecuali jika kita benar-benar yakin bahwa nilainya tidak mungkin null.

Type Checks and Casts 

Kotlin menyediakan fitur Type Checks dan Type Casts yang berfungsi untuk mengetahui tipe data suatu objek saat aplikasi sedang berjalan atau mengubah suatu objek menjadi tipe data tertentu.
Fitur ini banyak digunakan ketika menemukan objek yang memiliki tipe umum seperti Any atau struktur data yang dapat menyimpan berbagai jenis tipe data.

- Type Checks dengan is atau !is
Operator is digunakan untuk memeriksa apakah sebuah objek merupakan tipe data tertentu, sedangkan !is sebaliknya.


val name: Any = "Wim"

if (name is String) {
    println("Length: ${name.length}")
}

- Unsafe Cast dan Safe Cast
Unsafe Cast (as) digunakan untuk mengubah suatu objek menjadi tipe tertentu. Gunakan (as) hanya jika Anda yakin tipe datanya benar, karena kalau gagal akan menyebabkan error ClassCastException.

val name: Any = "Wim"

if (name is String) {
    println("Length: ${name.length}")
}

Sedangkan Safe Cast (as?) digunakan untuk menghandle terjadinya error ClassCastException dengan mengembalikan nilai null.

val data: Any = 100

val text = data as? String

println(text)

Output:

null

Jadi kesimpulannya Type Checks dan Type Casts menangani objek dengan tipe data yang berbeda secara aman.

Oke mungkin sekian dulu pembahasan kali ini, next kita akan membuat aplikasi android dengan kotlin. Semoga bermanfaat dan stay tuned untuk materi berikutnya. 

Terima kasih. ^^

(Tutorial Android) Android Image Slider

Tuesday, August 14, 2018 1 Comment

Hai guys, sudah sekian lama rasanya ane gak nulis di blog ini (kira2 setahun). Maklum guys karena ane belakangan ini sibuk banget jadi gak sempat bikin tutorial lagi, ditambah lagi juga malas yang berkepanjangan.. :v

Oke2, kali ini ane mau membahas bagaimana membuat Image Slider di Android. Temen2 pasti sudah tidak asing lagi dengan yang namanya Image Slider. Jadi, Image Slider merupakan tampilan seperti slide gambar yang bisa di geser satu-persatu utk menampilkannya.

Di Android terdapat fitur ViewPager yang bisa kita manfaatkan untuk membuat slider.

Langsung saja ke TKP.

Konfigurasi gradle dulu dengan menambahkan library Picasso.


…
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.0.1'
    compile 'com.squareup.picasso:picasso:2.5.2'
    testCompile 'junit:junit:4.12'
}


Buat kelas dengan nama CirclePageIndicator. Kelas ini merupakan custom view untuk menampilkan indicator slider.

Code ini pada dasarnya saya ambil dari https://github.com/JakeWharton/ViewPagerIndicator/blob/master/library/src/com/viewpagerindicator/CirclePageIndicator.java

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;


import id.co.blogspot.wimsonevel.android_imageslider.R;

import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;

/*
 * Copyright (C) 2011 Patrik Akerfeldt
 * Copyright (C) 2011 Jake Wharton
 *
 * 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.
 */

public class CirclePageIndicator extends View implements PageIndicator {

    private static final int INVALID_POINTER = -1;

    private float mRadius;
    private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG);
    private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG);
    private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG);
    private ViewPager mViewPager;
    private ViewPager.OnPageChangeListener mListener;
    private int mCurrentPage;
    private int mSnapPage;
    private float mPageOffset;
    private int mScrollState;
    private int mOrientation;
    private boolean mCentered;
    private boolean mSnap;

    private int mTouchSlop;
    private float mLastMotionX = -1;
    private int mActivePointerId = INVALID_POINTER;
    private boolean mIsDragging;


    public CirclePageIndicator(Context context) {
        this(context, null);
    }

    public CirclePageIndicator(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.vpiCirclePageIndicatorStyle);
    }

    public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        if (isInEditMode()) return;

        //Load defaults from resources
        final Resources res = getResources();
        final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color);
        final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color);
        final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation);
        final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color);
        final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width);
        final float defaultRadius = res.getDimension(R.dimen.viewpager_circleindicator_radius);
        final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered);
        final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap);

        //Retrieve styles attributes
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0);

        mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered);
        mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation);
        mPaintPageFill.setStyle(Paint.Style.FILL);
        mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor));
        mPaintStroke.setStyle(Paint.Style.STROKE);
        mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor));
        mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth));
        mPaintFill.setStyle(Paint.Style.FILL);
        mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor));
        mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius);
        mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap);

        Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background);
        if (background != null) {
            setBackgroundDrawable(background);
        }

        a.recycle();

        final ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
    }


    public void setCentered(boolean centered) {
        mCentered = centered;
        invalidate();
    }

    public boolean isCentered() {
        return mCentered;
    }

    public void setPageColor(int pageColor) {
        mPaintPageFill.setColor(pageColor);
        invalidate();
    }

    public int getPageColor() {
        return mPaintPageFill.getColor();
    }

    public void setFillColor(int fillColor) {
        mPaintFill.setColor(fillColor);
        invalidate();
    }

    public int getFillColor() {
        return mPaintFill.getColor();
    }

    public void setOrientation(int orientation) {
        switch (orientation) {
            case HORIZONTAL:
            case VERTICAL:
                mOrientation = orientation;
                requestLayout();
                break;

            default:
                throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL.");
        }
    }

    public int getOrientation() {
        return mOrientation;
    }

    public void setStrokeColor(int strokeColor) {
        mPaintStroke.setColor(strokeColor);
        invalidate();
    }

    public int getStrokeColor() {
        return mPaintStroke.getColor();
    }

    public void setStrokeWidth(float strokeWidth) {
        mPaintStroke.setStrokeWidth(strokeWidth);
        invalidate();
    }

    public float getStrokeWidth() {
        return mPaintStroke.getStrokeWidth();
    }

    public void setRadius(float radius) {
        mRadius = radius;
        invalidate();
    }

    public float getRadius() {
        return mRadius;
    }

    public void setSnap(boolean snap) {
        mSnap = snap;
        invalidate();
    }

    public boolean isSnap() {
        return mSnap;
    }

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

        if (mViewPager == null) {
            return;
        }
        final int count = mViewPager.getAdapter().getCount();
        if (count == 0) {
            return;
        }

        if (mCurrentPage >= count) {
            setCurrentItem(count - 1);
            return;
        }

        int longSize;
        int longPaddingBefore;
        int longPaddingAfter;
        int shortPaddingBefore;
        if (mOrientation == HORIZONTAL) {
            longSize = getWidth();
            longPaddingBefore = getPaddingLeft();
            longPaddingAfter = getPaddingRight();
            shortPaddingBefore = getPaddingTop();
        } else {
            longSize = getHeight();
            longPaddingBefore = getPaddingTop();
            longPaddingAfter = getPaddingBottom();
            shortPaddingBefore = getPaddingLeft();
        }

        final float threeRadius = mRadius * 3;
        final float shortOffset = shortPaddingBefore + mRadius;
        float longOffset = longPaddingBefore + mRadius;
        if (mCentered) {
            longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f);
        }

        float dX;
        float dY;

        float pageFillRadius = mRadius;
        if (mPaintStroke.getStrokeWidth() > 0) {
            pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f;
        }

        //Draw stroked circles
        for (int iLoop = 0; iLoop < count; iLoop++) {
            float drawLong = longOffset + (iLoop * threeRadius);
            if (mOrientation == HORIZONTAL) {
                dX = drawLong;
                dY = shortOffset;
            } else {
                dX = shortOffset;
                dY = drawLong;
            }
            // Only paint fill if not completely transparent
            if (mPaintPageFill.getAlpha() > 0) {
                canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill);
            }

            // Only paint stroke if a stroke width was non-zero
            if (pageFillRadius != mRadius) {
                canvas.drawCircle(dX, dY, mRadius, mPaintStroke);
            }
        }

        //Draw the filled circle according to the current scroll
        float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius;
        if (!mSnap) {
            cx += mPageOffset * threeRadius;
        }
        if (mOrientation == HORIZONTAL) {
            dX = longOffset + cx;
            dY = shortOffset;
        } else {
            dX = shortOffset;
            dY = longOffset + cx;
        }
        canvas.drawCircle(dX, dY, mRadius, mPaintFill);
    }

    public boolean onTouchEvent(MotionEvent ev) {
        if (super.onTouchEvent(ev)) {
            return true;
        }
        if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) {
            return false;
        }

        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mLastMotionX = ev.getX();
                break;

            case MotionEvent.ACTION_MOVE: {
                final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                final float x = MotionEventCompat.getX(ev, activePointerIndex);
                final float deltaX = x - mLastMotionX;

                if (!mIsDragging) {
                    if (Math.abs(deltaX) > mTouchSlop) {
                        mIsDragging = true;
                    }
                }

                if (mIsDragging) {
                    mLastMotionX = x;
                    if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) {
                        mViewPager.fakeDragBy(deltaX);
                    }
                }

                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (!mIsDragging) {
                    final int count = mViewPager.getAdapter().getCount();
                    final int width = getWidth();
                    final float halfWidth = width / 2f;
                    final float sixthWidth = width / 6f;

                    if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) {
                        if (action != MotionEvent.ACTION_CANCEL) {
                            mViewPager.setCurrentItem(mCurrentPage - 1);
                        }
                        return true;
                    } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) {
                        if (action != MotionEvent.ACTION_CANCEL) {
                            mViewPager.setCurrentItem(mCurrentPage + 1);
                        }
                        return true;
                    }
                }

                mIsDragging = false;
                mActivePointerId = INVALID_POINTER;
                if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag();
                break;

            case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int index = MotionEventCompat.getActionIndex(ev);
                mLastMotionX = MotionEventCompat.getX(ev, index);
                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                break;
            }

            case MotionEventCompat.ACTION_POINTER_UP:
                final int pointerIndex = MotionEventCompat.getActionIndex(ev);
                final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                if (pointerId == mActivePointerId) {
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
                }
                mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
                break;
        }

        return true;
    }

    @Override
    public void setViewPager(ViewPager view) {
        if (mViewPager == view) {
            return;
        }
        if (mViewPager != null) {
            mViewPager.setOnPageChangeListener(null);
        }
        if (view.getAdapter() == null) {
            throw new IllegalStateException("ViewPager does not have adapter instance.");
        }
        mViewPager = view;
        mViewPager.setOnPageChangeListener(this);
        invalidate();
    }

    @Override
    public void setViewPager(ViewPager view, int initialPosition) {
        setViewPager(view);
        setCurrentItem(initialPosition);
    }

    @Override
    public void setCurrentItem(int item) {
        if (mViewPager == null) {
            throw new IllegalStateException("ViewPager has not been bound.");
        }
        mViewPager.setCurrentItem(item);
        mCurrentPage = item;
        invalidate();
    }

    @Override
    public void notifyDataSetChanged() {
        invalidate();
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        mScrollState = state;

        if (mListener != null) {
            mListener.onPageScrollStateChanged(state);
        }
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        mCurrentPage = position;
        mPageOffset = positionOffset;
        invalidate();

        if (mListener != null) {
            mListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
        }
    }

    @Override
    public void onPageSelected(int position) {
        if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) {
            mCurrentPage = position;
            mSnapPage = position;
            invalidate();
        }

        if (mListener != null) {
            mListener.onPageSelected(position);
        }
    }

    @Override
    public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
        mListener = listener;
    }

    /*
     * (non-Javadoc)
     *
     * @see android.view.View#onMeasure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == HORIZONTAL) {
            setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec));
        } else {
            setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec));
        }
    }

    /**
     * Determines the width of this view
     *
     * @param measureSpec
     *            A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureLong(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {
            //We were told how big to be
            result = specSize;
        } else {
            //Calculate the width according the views count
            final int count = mViewPager.getAdapter().getCount();
            result = (int)(getPaddingLeft() + getPaddingRight()
                    + (count * 2 * mRadius) + (count - 1) * mRadius + 1);
            //Respect AT_MOST value if that was what is called for by measureSpec
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    /**
     * Determines the height of this view
     *
     * @param measureSpec
     *            A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureShort(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            //We were told how big to be
            result = specSize;
        } else {
            //Measure the height
            result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);
            //Respect AT_MOST value if that was what is called for by measureSpec
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        SavedState savedState = (SavedState)state;
        super.onRestoreInstanceState(savedState.getSuperState());
        mCurrentPage = savedState.currentPage;
        mSnapPage = savedState.currentPage;
        requestLayout();
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState savedState = new SavedState(superState);
        savedState.currentPage = mCurrentPage;
        return savedState;
    }

    static class SavedState extends BaseSavedState {
        int currentPage;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            currentPage = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(currentPage);
        }

        @SuppressWarnings("UnusedDeclaration")
        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}


Kemudian buat interface dengan nama PageIndicator.

Code nya juga saya ambil dari sini https://github.com/JakeWharton/ViewPagerIndicator/blob/master/library/src/com/viewpagerindicator/PageIndicator.java

/*
 * Copyright (C) 2011 Patrik Akerfeldt
 * Copyright (C) 2011 Jake Wharton
 *
 * 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.
 */

import android.support.v4.view.ViewPager;

/**
 * A PageIndicator is responsible to show an visual indicator on the total views
 * number and the current visible view.
 */

public interface PageIndicator extends ViewPager.OnPageChangeListener {

    /**
     * Bind the indicator to a ViewPager.
     *
     * @param view
     */
    void setViewPager(ViewPager view);

    /**
     * Bind the indicator to a ViewPager.
     *
     * @param view
     * @param initialPosition
     */
    void setViewPager(ViewPager view, int initialPosition);

    /**
     * <p>Set the current page of both the ViewPager and indicator.</p>
     *
     * <p>This <strong>must</strong> be used if you need to set the page before
     * the views are drawn on screen (e.g., default start page).</p>
     *
     * @param item
     */
    void setCurrentItem(int item);

    /**
     * Set a page change listener which will receive forwarded events.
     *
     * @param listener
     */
    void setOnPageChangeListener(ViewPager.OnPageChangeListener listener);

    /**
     * Notify the indicator that the fragment list has changed.
     */
    void notifyDataSetChanged();

}


Selanjutnya buat beberapa layout berikut :

list_item_pager.xml
Layout ini berisi ImageView untuk menampilkan image.

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

    <ImageView
        android:id="@+id/iv_header"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop" />

</RelativeLayout>

viewpager_slide.xml
Layout berisi ViewPager dan Custom CirclePageIndicator.

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

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@null" />

    <id.co.blogspot.wimsonevel.android_imageslider.widget.CirclePageIndicator
        android:id="@+id/indicator"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:padding="10dp" />

</RelativeLayout>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="id.co.blogspot.wimsonevel.android_imageslider.MainActivity">

    <include
        layout="@layout/viewpager_slide" />

</RelativeLayout>

Tambahkan beberapa resource values yang diperlukan.

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="centered" format="boolean" />
    <attr name="selectedColor" format="color" />
    <attr name="strokeWidth" format="dimension" />
    <attr name="unselectedColor" format="color" />

    <!-- Circle Page Indicator -->
    <declare-styleable name="ViewPagerIndicator">
        <!-- Style of the circle indicator. -->
        <attr name="vpiCirclePageIndicatorStyle" format="reference"/>
        <!-- Style of the icon indicator's views. -->
        <attr name="vpiIconPageIndicatorStyle" format="reference"/>
        <!-- Style of the line indicator. -->
        <attr name="vpiLinePageIndicatorStyle" format="reference"/>
        <!-- Style of the title indicator. -->
        <attr name="vpiTitlePageIndicatorStyle" format="reference"/>
        <!-- Style of the tab indicator's tabs. -->
        <attr name="vpiTabPageIndicatorStyle" format="reference"/>
        <!-- Style of the underline indicator. -->
        <attr name="vpiUnderlinePageIndicatorStyle" format="reference"/>
    </declare-styleable>

    <declare-styleable name="CirclePageIndicator">
        <!-- Whether or not the indicators should be centered. -->
        <attr name="centered" />
        <!-- Color of the filled circle that represents the current page. -->
        <attr name="fillColor" format="color" />
        <!-- Color of the filled circles that represents pages. -->
        <attr name="pageColor" format="color" />
        <!-- Orientation of the indicator. -->
        <attr name="android:orientation"/>
        <!-- Radius of the circles. This is also the spacing between circles. -->
        <attr name="radius" format="dimension" />
        <!-- Whether or not the selected indicator snaps to the circles. -->
        <attr name="snap" format="boolean" />
        <!-- Color of the open circles. -->
        <attr name="strokeColor" format="color" />
        <!-- Width of the stroke used to draw the circles. -->
        <attr name="strokeWidth" />
        <!-- View background -->
        <attr name="android:background"/>
    </declare-styleable>


</resources>

bools.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="default_circle_indicator_centered">true</bool>
    <bool name="default_circle_indicator_snap">false</bool>
</resources>

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#5d4037</color>
    <color name="colorPrimaryDark">#321911</color>
    <color name="colorAccent">#FF4081</color>
    <color name="colorWhite">#FFFFFF</color>

    <!-- Circle Page Indicator -->
    <color name="default_circle_indicator_fill_color">@color/colorPrimary</color>
    <color name="default_circle_indicator_page_color">@color/colorWhite</color>
    <color name="default_circle_indicator_stroke_color">@android:color/transparent</color>

</resources>

integers.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- Circle Page Indicator -->
    <integer name="default_circle_indicator_orientation">0</integer>

</resources>

Kembali ke kelas java, buat kelas dengan nama ImagePagerAdapter.

import android.content.Context;
import android.support.v4.view.PagerAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;

import com.squareup.picasso.Picasso;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by kwikkunusantara on 8/10/18.
 */

public class ImagePagerAdapter extends PagerAdapter {

    private Context context;
    private List<String> images;

    public ImagePagerAdapter(Context context) {
        this.context = context;
        this.images = new ArrayList<>();
    }

    public void setImages(List<String> images) {
        for (int i = 0; i < images.size(); i++){
            this.images.add(images.get(i));
        }
        notifyDataSetChanged();
    }

    @Override
    public int getCount() {
        return images.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        String image = images.get(position);
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View itemView = inflater.inflate(R.layout.item_image_pager, container, false);
        ImageView ivHeader = (ImageView) itemView.findViewById(R.id.iv_header);

        Picasso.with(context)
                .load(image)
                .into(ivHeader);

        container.addView(itemView);

        return itemView;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((RelativeLayout) object);
    }
}


Setelah itu tambahkan code berikut di MainActivity

Di sini saya menambahkan Timer supaya Slider-nya bisa bergerak otomatis. Beberapa image saya ambil dari link Facebook nya Aliga <3. *Dasar Jones

import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import id.co.blogspot.wimsonevel.android_imageslider.widget.CirclePageIndicator;

public class MainActivity extends AppCompatActivity {

    private ViewPager viewPager;
    private CirclePageIndicator pagerIndicator;

    private ImagePagerAdapter imagePagerAdapter;

    private Timer timer;

    private List<String> images;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        viewPager = (ViewPager) findViewById(R.id.pager);
        pagerIndicator = (CirclePageIndicator) findViewById(R.id.indicator);

        imagePagerAdapter = new ImagePagerAdapter(this);

        viewPager.setAdapter(imagePagerAdapter);
        pagerIndicator.setViewPager(viewPager);

        images = new ArrayList<>();
        images.add("https://scontent.fcgk12-1.fna.fbcdn.net/v/t1.0-9/30260981_1697358283684452_5345280777774956544_n.jpg?_nc_cat=0&oh=780b2eba44f38c3400b8e219060b3278&oe=5C08A56E");
        images.add("https://scontent.fcgk12-1.fna.fbcdn.net/v/t1.0-9/30412288_1697358530351094_6652467221207449600_n.jpg?_nc_cat=0&oh=9c39ae9327064d3c21490a556acf28a3&oe=5C137E2E");
        images.add("https://scontent.fcgk12-1.fna.fbcdn.net/v/t1.0-9/30412267_1697358943684386_6572812530102566912_n.jpg?_nc_cat=0&oh=1876343af22cad2f49d352343eb3eb6b&oe=5C12B116");
        images.add("https://scontent.fcgk12-1.fna.fbcdn.net/v/t1.0-9/30261069_1697359000351047_8720462127048949760_n.jpg?_nc_cat=0&oh=44a1c82bebff0f61b811ef2bb94b3196&oe=5BC978E2");

        imagePagerAdapter.setImages(images);

        timer = new Timer();
        timer.scheduleAtFixedRate(new SliderTimer(), 3000, 5000);
    }

    private class SliderTimer extends TimerTask {

        @Override
        public void run() {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (viewPager.getCurrentItem() < imagePagerAdapter.getCount() - 1) {
                        viewPager.setCurrentItem(viewPager.getCurrentItem() + 1);
                    } else {
                        viewPager.setCurrentItem(0);
                    }
                }
            });
        };
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        timer.cancel();
    }
}

Terakhir tambahkan permission INTERNET di AndroidManifest.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="id.co.blogspot.wimsonevel.android_imageslider">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Build dan jalankan maka hasinya sebagai berikut :


Source code lengkap dapat dilihat di https://github.com/wimsonevel/Android-ImageSlider
Sekian dulu untuk tutorial kali ini dan semoga bermanfaat.

Jangan lupa share ^^

(Tutorial Android) Countdown Timer with Circular Progress Animation

Thursday, August 10, 2017 Add Comment

Countdown merupakan istilah dari bahasa inggris yang artinya hitung mundur. Android menyediakan kelas yang berfungsi untuk menghitung mundur yang dinamakan CountDownTimer. Untuk membuat program countdown, kita tinggal mengimplementasikan kelas tersebut. Pada kesempatan kali ini saya akan membagikan sedikit tutorial hasil dari request teman-teman. Kita akan membuat program countdown dengan animasi circular progress.

Oke langsung saja kita ke TKP.

Pertama, buat project baru (wajib hehehe).

Selanjutnya kita membuat circular progress. Di sini kita harus membuat custom view sendiri untuk membuat circular progress, karena di Android sendiri memang tidak disediakan. Berikut adalah codenya saya ambil dan modifikasi sedikit dari https://github.com/lzyzsd/CircleProgress. Beri nama DonutProgress.java :

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by bruce on 11/4/14.
 */

public class DonutProgress extends View {

    private Paint finishedPaint;
    private Paint unfinishedPaint;
    private Paint innerCirclePaint;

    protected Paint textPaint;
    protected Paint innerBottomTextPaint;

    private RectF finishedOuterRect = new RectF();
    private RectF unfinishedOuterRect = new RectF();

    private int attributeResourceId = 0;
    private boolean showText;
    private float textSize;
    private int textColor;
    private int innerBottomTextColor;
    private int progress = 0;
    private int max;
    private int finishedStrokeColor;
    private int unfinishedStrokeColor;
    private int startingDegree;
    private float finishedStrokeWidth;
    private float unfinishedStrokeWidth;
    private int innerBackgroundColor;
    private String prefixText = "";
    private String suffixText = "";
    private String text = null;
    private float innerBottomTextSize;
    private String innerBottomText;
    private float innerBottomTextHeight;

    private final float default_stroke_width;
    private final int default_finished_color = Color.rgb(66, 145, 241);
    private final int default_unfinished_color = Color.rgb(204, 204, 204);
    private final int default_text_color = Color.rgb(66, 145, 241);
    private final int default_inner_bottom_text_color = Color.rgb(66, 145, 241);
    private final int default_inner_background_color = Color.TRANSPARENT;
    private final int default_max = 100;
    private final int default_startingDegree = 0;
    private final float default_text_size;
    private final float default_inner_bottom_text_size;
    private final int min_size;


    private static final String INSTANCE_STATE = "saved_instance";
    private static final String INSTANCE_TEXT_COLOR = "text_color";
    private static final String INSTANCE_TEXT_SIZE = "text_size";
    private static final String INSTANCE_TEXT = "text";
    private static final String INSTANCE_INNER_BOTTOM_TEXT_SIZE = "inner_bottom_text_size";
    private static final String INSTANCE_INNER_BOTTOM_TEXT = "inner_bottom_text";
    private static final String INSTANCE_INNER_BOTTOM_TEXT_COLOR = "inner_bottom_text_color";
    private static final String INSTANCE_FINISHED_STROKE_COLOR = "finished_stroke_color";
    private static final String INSTANCE_UNFINISHED_STROKE_COLOR = "unfinished_stroke_color";
    private static final String INSTANCE_MAX = "max";
    private static final String INSTANCE_PROGRESS = "progress";
    private static final String INSTANCE_SUFFIX = "suffix";
    private static final String INSTANCE_PREFIX = "prefix";
    private static final String INSTANCE_FINISHED_STROKE_WIDTH = "finished_stroke_width";
    private static final String INSTANCE_UNFINISHED_STROKE_WIDTH = "unfinished_stroke_width";
    private static final String INSTANCE_BACKGROUND_COLOR = "inner_background_color";
    private static final String INSTANCE_STARTING_DEGREE = "starting_degree";
    private static final String INSTANCE_INNER_DRAWABLE = "inner_drawable";

    public DonutProgress(Context context) {
        this(context, null);
    }

    public DonutProgress(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DonutProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        default_text_size = Utils.sp2px(getResources(), 18);
        min_size = (int) Utils.dp2px(getResources(), 100);
        default_stroke_width = Utils.dp2px(getResources(), 10);
        default_inner_bottom_text_size = Utils.sp2px(getResources(), 18);

        final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DonutProgress, defStyleAttr, 0);
        initByAttributes(attributes);
        attributes.recycle();

        initPainters();
    }

    protected void initPainters() {
        if (showText) {
            textPaint = new TextPaint();
            textPaint.setColor(textColor);
            textPaint.setTextSize(textSize);
            textPaint.setAntiAlias(true);

            innerBottomTextPaint = new TextPaint();
            innerBottomTextPaint.setColor(innerBottomTextColor);
            innerBottomTextPaint.setTextSize(innerBottomTextSize);
            innerBottomTextPaint.setAntiAlias(true);
        }

        finishedPaint = new Paint();
        finishedPaint.setColor(finishedStrokeColor);
        finishedPaint.setStyle(Paint.Style.STROKE);
        finishedPaint.setAntiAlias(true);
        finishedPaint.setStrokeWidth(finishedStrokeWidth);

        unfinishedPaint = new Paint();
        unfinishedPaint.setColor(unfinishedStrokeColor);
        unfinishedPaint.setStyle(Paint.Style.STROKE);
        unfinishedPaint.setAntiAlias(true);
        unfinishedPaint.setStrokeWidth(unfinishedStrokeWidth);

        innerCirclePaint = new Paint();
        innerCirclePaint.setColor(innerBackgroundColor);
        innerCirclePaint.setAntiAlias(true);
    }

    protected void initByAttributes(TypedArray attributes) {
        finishedStrokeColor = attributes.getColor(R.styleable.DonutProgress_donut_finished_color, default_finished_color);
        unfinishedStrokeColor = attributes.getColor(R.styleable.DonutProgress_donut_unfinished_color, default_unfinished_color);
        showText = attributes.getBoolean(R.styleable.DonutProgress_donut_show_text, true);
        attributeResourceId = attributes.getResourceId(R.styleable.DonutProgress_donut_inner_drawable, 0);

        setMax(attributes.getInt(R.styleable.DonutProgress_donut_max, default_max));
        setProgress(attributes.getInt(R.styleable.DonutProgress_donut_progress, 0));
        finishedStrokeWidth = attributes.getDimension(R.styleable.DonutProgress_donut_finished_stroke_width, default_stroke_width);
        unfinishedStrokeWidth = attributes.getDimension(R.styleable.DonutProgress_donut_unfinished_stroke_width, default_stroke_width);

        if (showText) {
            if (attributes.getString(R.styleable.DonutProgress_donut_prefix_text) != null) {
                prefixText = attributes.getString(R.styleable.DonutProgress_donut_prefix_text);
            }
            if (attributes.getString(R.styleable.DonutProgress_donut_suffix_text) != null) {
                suffixText = attributes.getString(R.styleable.DonutProgress_donut_suffix_text);
            }
            if (attributes.getString(R.styleable.DonutProgress_donut_text) != null) {
                text = attributes.getString(R.styleable.DonutProgress_donut_text);
            }

            textColor = attributes.getColor(R.styleable.DonutProgress_donut_text_color, default_text_color);
            textSize = attributes.getDimension(R.styleable.DonutProgress_donut_text_size, default_text_size);
            innerBottomTextSize = attributes.getDimension(R.styleable.DonutProgress_donut_inner_bottom_text_size, default_inner_bottom_text_size);
            innerBottomTextColor = attributes.getColor(R.styleable.DonutProgress_donut_inner_bottom_text_color, default_inner_bottom_text_color);
            innerBottomText = attributes.getString(R.styleable.DonutProgress_donut_inner_bottom_text);
        }

        innerBottomTextSize = attributes.getDimension(R.styleable.DonutProgress_donut_inner_bottom_text_size, default_inner_bottom_text_size);
        innerBottomTextColor = attributes.getColor(R.styleable.DonutProgress_donut_inner_bottom_text_color, default_inner_bottom_text_color);
        innerBottomText = attributes.getString(R.styleable.DonutProgress_donut_inner_bottom_text);

        startingDegree = attributes.getInt(R.styleable.DonutProgress_donut_circle_starting_degree, default_startingDegree);
        innerBackgroundColor = attributes.getColor(R.styleable.DonutProgress_donut_background_color, default_inner_background_color);
    }

    @Override
    public void invalidate() {
        initPainters();
        super.invalidate();
    }

    public boolean isShowText() {
        return showText;
    }

    public void setShowText(boolean showText) {
        this.showText = showText;
    }

    public float getFinishedStrokeWidth() {
        return finishedStrokeWidth;
    }

    public void setFinishedStrokeWidth(float finishedStrokeWidth) {
        this.finishedStrokeWidth = finishedStrokeWidth;
        this.invalidate();
    }

    public float getUnfinishedStrokeWidth() {
        return unfinishedStrokeWidth;
    }

    public void setUnfinishedStrokeWidth(float unfinishedStrokeWidth) {
        this.unfinishedStrokeWidth = unfinishedStrokeWidth;
        this.invalidate();
    }

    private float getProgressAngle() {
        return getProgress() / (float) max * 360f;
    }

    public float getProgress() {
        return progress;
    }

    public void setProgress(int progress) {
        this.progress = progress;
        if (this.progress > getMax()) {
            this.progress %= getMax();
        }
        invalidate();
    }

    public int getMax() {
        return max;
    }

    public void setMax(int max) {
        if (max > 0) {
            this.max = max;
            invalidate();
        }
    }

    public float getTextSize() {
        return textSize;
    }

    public void setTextSize(float textSize) {
        this.textSize = textSize;
        this.invalidate();
    }

    public int getTextColor() {
        return textColor;
    }

    public void setTextColor(int textColor) {
        this.textColor = textColor;
        this.invalidate();
    }

    public int getFinishedStrokeColor() {
        return finishedStrokeColor;
    }

    public void setFinishedStrokeColor(int finishedStrokeColor) {
        this.finishedStrokeColor = finishedStrokeColor;
        this.invalidate();
    }

    public int getUnfinishedStrokeColor() {
        return unfinishedStrokeColor;
    }

    public void setUnfinishedStrokeColor(int unfinishedStrokeColor) {
        this.unfinishedStrokeColor = unfinishedStrokeColor;
        this.invalidate();
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
        this.invalidate();
    }

    public String getSuffixText() {
        return suffixText;
    }

    public void setSuffixText(String suffixText) {
        this.suffixText = suffixText;
        this.invalidate();
    }

    public String getPrefixText() {
        return prefixText;
    }

    public void setPrefixText(String prefixText) {
        this.prefixText = prefixText;
        this.invalidate();
    }

    public int getInnerBackgroundColor() {
        return innerBackgroundColor;
    }

    public void setInnerBackgroundColor(int innerBackgroundColor) {
        this.innerBackgroundColor = innerBackgroundColor;
        this.invalidate();
    }


    public String getInnerBottomText() {
        return innerBottomText;
    }

    public void setInnerBottomText(String innerBottomText) {
        this.innerBottomText = innerBottomText;
        this.invalidate();
    }


    public float getInnerBottomTextSize() {
        return innerBottomTextSize;
    }

    public void setInnerBottomTextSize(float innerBottomTextSize) {
        this.innerBottomTextSize = innerBottomTextSize;
        this.invalidate();
    }

    public int getInnerBottomTextColor() {
        return innerBottomTextColor;
    }

    public void setInnerBottomTextColor(int innerBottomTextColor) {
        this.innerBottomTextColor = innerBottomTextColor;
        this.invalidate();
    }

    public int getStartingDegree() {
        return startingDegree;
    }

    public void setStartingDegree(int startingDegree) {
        this.startingDegree = startingDegree;
        this.invalidate();
    }

    public int getAttributeResourceId() {
        return attributeResourceId;
    }

    public void setAttributeResourceId(int attributeResourceId) {
        this.attributeResourceId = attributeResourceId;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measure(widthMeasureSpec), measure(heightMeasureSpec));

        //TODO calculate inner circle height and then position bottom text at the bottom (3/4)
        innerBottomTextHeight = getHeight() - (getHeight() * 3) / 4;
    }

    private int measure(int measureSpec) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = min_size;
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }

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

        float delta = Math.max(finishedStrokeWidth, unfinishedStrokeWidth);
        finishedOuterRect.set(delta,
                delta,
                getWidth() - delta,
                getHeight() - delta);
        unfinishedOuterRect.set(delta,
                delta,
                getWidth() - delta,
                getHeight() - delta);

        float innerCircleRadius = (getWidth() - Math.min(finishedStrokeWidth, unfinishedStrokeWidth) + Math.abs(finishedStrokeWidth - unfinishedStrokeWidth)) / 2f;
        canvas.drawCircle(getWidth() / 2.0f, getHeight() / 2.0f, innerCircleRadius, innerCirclePaint);
        canvas.drawArc(finishedOuterRect, getStartingDegree(), getProgressAngle(), false, finishedPaint);
        canvas.drawArc(unfinishedOuterRect, getStartingDegree() + getProgressAngle(), 360 - getProgressAngle(), false, unfinishedPaint);

        if (showText) {
            String text = this.text != null ? this.text : prefixText + progress + suffixText;
            if (!TextUtils.isEmpty(text)) {
                float textHeight = textPaint.descent() + textPaint.ascent();
                canvas.drawText(text, (getWidth() - textPaint.measureText(text)) / 2.0f, (getWidth() - textHeight) / 2.0f, textPaint);
            }

            if (!TextUtils.isEmpty(getInnerBottomText())) {
                innerBottomTextPaint.setTextSize(innerBottomTextSize);
                float bottomTextBaseline = getHeight() - innerBottomTextHeight - (textPaint.descent() + textPaint.ascent()) / 2;
                canvas.drawText(getInnerBottomText(), (getWidth() - innerBottomTextPaint.measureText(getInnerBottomText())) / 2.0f, bottomTextBaseline, innerBottomTextPaint);
            }
        }

        if (attributeResourceId != 0) {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), attributeResourceId);
            canvas.drawBitmap(bitmap, (getWidth() - bitmap.getWidth()) / 2.0f, (getHeight() - bitmap.getHeight()) / 2.0f, null);
        }
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        final Bundle bundle = new Bundle();
        bundle.putParcelable(INSTANCE_STATE, super.onSaveInstanceState());
        bundle.putInt(INSTANCE_TEXT_COLOR, getTextColor());
        bundle.putFloat(INSTANCE_TEXT_SIZE, getTextSize());
        bundle.putFloat(INSTANCE_INNER_BOTTOM_TEXT_SIZE, getInnerBottomTextSize());
        bundle.putFloat(INSTANCE_INNER_BOTTOM_TEXT_COLOR, getInnerBottomTextColor());
        bundle.putString(INSTANCE_INNER_BOTTOM_TEXT, getInnerBottomText());
        bundle.putInt(INSTANCE_INNER_BOTTOM_TEXT_COLOR, getInnerBottomTextColor());
        bundle.putInt(INSTANCE_FINISHED_STROKE_COLOR, getFinishedStrokeColor());
        bundle.putInt(INSTANCE_UNFINISHED_STROKE_COLOR, getUnfinishedStrokeColor());
        bundle.putInt(INSTANCE_MAX, getMax());
        bundle.putInt(INSTANCE_STARTING_DEGREE, getStartingDegree());
        bundle.putFloat(INSTANCE_PROGRESS, getProgress());
        bundle.putString(INSTANCE_SUFFIX, getSuffixText());
        bundle.putString(INSTANCE_PREFIX, getPrefixText());
        bundle.putString(INSTANCE_TEXT, getText());
        bundle.putFloat(INSTANCE_FINISHED_STROKE_WIDTH, getFinishedStrokeWidth());
        bundle.putFloat(INSTANCE_UNFINISHED_STROKE_WIDTH, getUnfinishedStrokeWidth());
        bundle.putInt(INSTANCE_BACKGROUND_COLOR, getInnerBackgroundColor());
        bundle.putInt(INSTANCE_INNER_DRAWABLE, getAttributeResourceId());
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            final Bundle bundle = (Bundle) state;
            textColor = bundle.getInt(INSTANCE_TEXT_COLOR);
            textSize = bundle.getFloat(INSTANCE_TEXT_SIZE);
            innerBottomTextSize = bundle.getFloat(INSTANCE_INNER_BOTTOM_TEXT_SIZE);
            innerBottomText = bundle.getString(INSTANCE_INNER_BOTTOM_TEXT);
            innerBottomTextColor = bundle.getInt(INSTANCE_INNER_BOTTOM_TEXT_COLOR);
            finishedStrokeColor = bundle.getInt(INSTANCE_FINISHED_STROKE_COLOR);
            unfinishedStrokeColor = bundle.getInt(INSTANCE_UNFINISHED_STROKE_COLOR);
            finishedStrokeWidth = bundle.getFloat(INSTANCE_FINISHED_STROKE_WIDTH);
            unfinishedStrokeWidth = bundle.getFloat(INSTANCE_UNFINISHED_STROKE_WIDTH);
            innerBackgroundColor = bundle.getInt(INSTANCE_BACKGROUND_COLOR);
            attributeResourceId = bundle.getInt(INSTANCE_INNER_DRAWABLE);
            initPainters();
            setMax(bundle.getInt(INSTANCE_MAX));
            setStartingDegree(bundle.getInt(INSTANCE_STARTING_DEGREE));
            setProgress(bundle.getInt(INSTANCE_PROGRESS));
            suffixText = bundle.getString(INSTANCE_SUFFIX);
            prefixText = bundle.getString(INSTANCE_PREFIX);
            text = bundle.getString(INSTANCE_TEXT);
            super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATE));
            return;
        }
        super.onRestoreInstanceState(state);
    }
    public void setDonut_progress(String percent){
        if(!TextUtils.isEmpty(percent)){
            setProgress(Integer.parseInt(percent));
        }
    }

}


Kemudian kelas Utils.java

import android.content.res.Resources;

/**
 * Created by bruce on 14-11-6.
 */

public final class Utils {

    private Utils() {
    }

    public static float dp2px(Resources resources, float dp) {
        final float scale = resources.getDisplayMetrics().density;
        return  dp * scale + 0.5f;
    }

    public static float sp2px(Resources resources, float sp){
        final float scale = resources.getDisplayMetrics().scaledDensity;
        return sp * scale;
    }

}

Buat layout dengan nama activity_main.xml. Untuk memanggil custom view harus diikuti dengan nama package.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="id.co.blogspot.wimsonevel.countdown_animation.MainActivity">

    <id.co.blogspot.wimsonevel.countdown_animation.DonutProgress
        android:id="@+id/countdown_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        custom:donut_progress="10"
        custom:donut_max="10"
        custom:donut_circle_starting_degree="270"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />

    <Button
        android:id="@+id/btn_start"
        android:text="@string/start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/countdown_progress"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="64dp" />

</RelativeLayout>

Tambahkan beberapa resource values berikut :

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="DonutProgress">
        <attr name="donut_progress" format="integer"/>
        <attr name="donut_max" format="integer"/>

        <attr name="donut_unfinished_color" format="color"/>
        <attr name="donut_finished_color" format="color"/>

        <attr name="donut_finished_stroke_width" format="dimension"/>
        <attr name="donut_unfinished_stroke_width" format="dimension"/>

        <attr name="donut_text_size" format="dimension"/>
        <attr name="donut_text_color" format="color"/>

        <attr name="donut_prefix_text" format="string"/>
        <attr name="donut_suffix_text" format="string"/>
        <attr name="donut_text" format="string"/>

        <attr name="donut_background_color" format="color"/>

        <attr name="donut_inner_bottom_text" format="string"/>
        <attr name="donut_inner_bottom_text_size" format="dimension"/>
        <attr name="donut_inner_bottom_text_color" format="color"/>

        <attr name="donut_circle_starting_degree" format="integer" />
        <attr name="donut_show_text" format="boolean"/>
        <attr name="donut_inner_drawable" format="reference"/>
    </declare-styleable>

</resources>

strings.xml

<resources>
    <string name="app_name">Countdown-Animation</string>
    <string name="start">Start</string>
    <string name="stop">Stop</string>
    <string name="reset">Reset</string>
</resources>

Terakhir implementasikan CountDownTimer di kelas MainActivity.java berikut :

import android.os.CountDownTimer;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private CountDownTimer countDownTimer;
    private DonutProgress countDownProgress;
    private Button btnStart;

    private int status = 0;
    private final long startTime = 10 * 1000;
    private final long interval = 1 * 1000;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        countDownProgress = (DonutProgress) findViewById(R.id.countdown_progress);
        btnStart = (Button) findViewById(R.id.btn_start);

        countDownProgress.setProgress(10);

        btnStart.setOnClickListener(this);

        countDownTimer = new CountDownTimer(startTime, interval) {
            @Override
            public void onTick(long millisUntilFinished) {
                countDownProgress.setProgress((int) millisUntilFinished / 1000);
            }

            @Override
            public void onFinish() {
                countDownProgress.setProgress(0);
                status = 2;
                btnStart.setText(R.string.reset);
            }
        };
    }

    @Override
    public void onClick(View view) {
        if (status == 0) {
            countDownTimer.start();
            status = 1;
            btnStart.setText(R.string.stop);
        } else if(status == 1){
            countDownTimer.cancel();
            countDownProgress.setProgress(10);
            status = 0;
            btnStart.setText(R.string.start);
        } else {
            countDownProgress.setProgress(10);
            status = 0;
            btnStart.setText(R.string.start);
        }
    }
}


Build dan jalankan maka hasilnya sebagai berikut :


Source code bisa di download di https://github.com/wimsonevel/Countdown-Animation

Sekian tutorial kali ini semoga bermanfaat.
Jangan lupa share ke sosial media kalian ya :)

(Tutorial Android) Download Image to External Storage using Picasso

Saturday, July 29, 2017 3 Comments

Pada tutorial sebelumnya http://wimsonevel.blogspot.co.id/2016/05/tutorial-android-image-loader-using.html saya menjelaskan bagaimana penggunaan library Picasso sebagai library Image Loader yang berfungsi menampilkan gambar dari penyimpanan lokal maupun link url. Nah, pada kesempatan kali ini saya akan membagikan sedikit code agar gambar yang sudah di-load oleh Picasso bisa didownload atau disimpan di memori penyimpanan.

Langsung aja ke TKP.

Pertama buat project baru agan-agan.

Tambahkan library Picasso di build.gradle

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.squareup.picasso:picasso:2.5.2'
    testCompile 'junit:junit:4.12'
}

Kedua buat layout dengan activity_main.xml dengan komponen Image View dan Button.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="id.co.blogspot.wimsonevel.android_picassodownload.MainActivity">

    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:layout_gravity="center"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:layout_above="@+id/btn_save"
        tools:ignore="ContentDescription" />

    <Button
        android:id="@+id/btn_save"
        android:text="@string/save_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true" />

</RelativeLayout>


Kemudian beberapa resouce lainnya berikut :

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#b94948</color>
    <color name="colorPrimaryDark">#b94948</color>
    <color name="colorAccent">#FF4081</color>
</resources>

strings.xml

<resources>
    <string name="app_name">Android-PicassoDownload</string>

    <string name="save_image">Save Image</string>

</resources>


Picasso Target
Picasso menyediakan sebuat interface Target yang menyediakan callback bitmap sehingga kita dapat memanfaatkan bitmap dari image untuk diproses lebih lanjut.

Contoh penggunaannya :

Picasso.with(this)
        .load(url)
        .into(new Target() {
            @Override
            public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
                
            }

            @Override
            public void onBitmapFailed(Drawable errorDrawable) {

            }

            @Override
            public void onPrepareLoad(Drawable placeHolderDrawable) {
            }
        });

Hasil bitmap lalu kita proses untuk dijadikan sebuah file image ber-ekstensi jpg. Kemudian disimpan di direktori folder memori eksternal yang kita inginkan. Contohnya saya menyimpan file image di folder Pictures.

File sd = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
File folder = new File(sd, "/PicassoDownload/");
if (!folder.exists()) {
    if (!folder.mkdir()) {
        Log.e("ERROR", "Cannot create a directory!");
    } else {
        folder.mkdir();
    }
}
File fileName = new File(folder, imgName);
try {
    fileName.createNewFile();
    FileOutputStream ostream = new FileOutputStream(fileName);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, ostream);
    ostream.close();
} catch (Exception e) {
    e.printStackTrace();
}

Buat activity dengan nama MainActivity.java, implementasikan code di atas di dalamnya ketika tombol save diklik. Untuk memodifikasi baik itu membaca maupun menulis file di memori eksternal kita memerlukan permission, di Android versi M ke atas perlu adanya pengecekan permission secara runtime.

Berikut list kode lengkapnya.

import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;

import java.io.File;
import java.io.FileOutputStream;

public class MainActivity extends AppCompatActivity {

    private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 100;
    private static final String PREF_CAMERA_REQUESTED = "cameraRequested";

    private String url;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageView ivImage= (ImageView) findViewById(R.id.iv_image);
        Button btnSave = (Button) findViewById(R.id.btn_save);

        url = "https://scontent-sit4-1.xx.fbcdn.net/v/t1.0-9/20228885_1440264296060520_6773935769024601349_n.jpg?oh=da445041a4bc99ec499b78d39b8832eb&oe=5A371E89";

        Picasso.with(this)
                .load(url)
                .into(ivImage);

        btnSave.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                saveImage(url, "newfile.jpg");
            }
        });
    }

    private void saveImage(String url, final String imgName) {
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
                == PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "In progress...", Toast.LENGTH_SHORT).show();

            Picasso.with(this)
                    .load(url)
                    .into(new Target() {
                        @Override
                        public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
                            File sd = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
                            File folder = new File(sd, "/PicassoDownload/");
                            if (!folder.exists()) {
                                if (!folder.mkdir()) {
                                    Log.e("ERROR", "Cannot create a directory!");
                                } else {
                                    folder.mkdir();
                                }
                            }

                            File fileName = new File(folder, imgName);

                            try {
                                fileName.createNewFile();
                                FileOutputStream ostream = new FileOutputStream(fileName);
                                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, ostream);
                                ostream.close();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                            Toast.makeText(MainActivity.this, "Image Saved Successfully!", Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void onBitmapFailed(Drawable errorDrawable) {
                            Toast.makeText(MainActivity.this, "Image Failed to Save", Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void onPrepareLoad(Drawable placeHolderDrawable) {

                        }
                    });
        } else {
            requestWriteExternalPermission();
        }
    }

    private void requestWriteExternalPermission() {
        final String[] permissions = new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE};

        if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
        } else {
            if (!isPermissionRequested(PREF_CAMERA_REQUESTED)) {
                ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
                setPermissionRequested(PREF_CAMERA_REQUESTED);
            } else {
                Toast.makeText(MainActivity.this, "Please grant storage permission to save images", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void setPermissionRequested(String permission) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = preferences.edit();
        editor.putBoolean(permission, true);
        editor.apply();
    }

    private boolean isPermissionRequested(String permission) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        return preferences.getBoolean(permission, false);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

        switch (requestCode) {
            case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE: {
                if (grantResults.length != 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Log.d("SUCCESS", "Write External permission granted");
                    saveImage(url, "newFile.jpg");
                    return;
                }
                Log.e("ERROR", "Permission not granted: results len = " + grantResults.length +
                        " Result code = " + (grantResults.length > 0 ? grantResults[0] : "(empty)"));
                finish();
            }
            default: {
                Log.d("ERROR", "Got unexpected permission result: " + requestCode);
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
                break;
            }
        }
    }
}

Terakhir tambahkan permission di AndroidManifest.xml.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="id.co.blogspot.wimsonevel.android_picassodownload">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Build dan jalankan maka hasilnya sebagai berikut :


Klik “Save Image” maka image akan tersimpan di folder Pictures->PicassoDownload

Source code lengkap dapat dilihat di https://github.com/wimsonevel/Android-PicassoDownload

Sekian tutorial kali ini dan semoga bermanfaat.
Jangan lupa share ke sosial media kalian ^^