React Native ile Mobil Uygulama Geliştirme – 11 – Hooks ve FlatList ile Search Bar Ekleme

“React Native ile Mobil Uygulama Geliştirme” başlıklı makalelerimizden bir yenisini daha sizlerle paylaşmak istiyorum. Bu makale, bir dizi makaleden oluşacak olan serisinin 11. kısmıdır. Bir önceki makalede (Part 10), React Native’de Drag & Drop bileşenini oluşturmayı ve mobil projelerimizde nasıl kullanacağımızı anlattım.

React Native’de Hooks ve FlatList Kullanarak Search Bar bileşenini eklemeyi ve mobil projelerimizde nasıl kullanacağımızı öğreneceğiz.

React Native ile mobil uygulamalar geliştirirken verileri görüntülemek için yaygın bir kullanım durumu, bir liste biçimindedir.

React Native’de liste oluşturmanın iki yaygın yolu vardır: ScrollView ve FlatList. Framework’ün API’sindeki bu bileşenlerin her birinin kendi gücü vardır.

Bu makalede, verileri almak, verileri görüntülemek ve bir arama çubuğu eklemek için FlatList tarafından sağlanan farklı unsurları inceleyelim.

Daha önceki makaleleri okumadıysanız, lütfen buradan başlayın.

Expo tabanlı yeni bir proje oluşturmak için, bir terminal penceresi açın ve aşağıdaki komutları açıklanan sırayla çalıştırın.

Proje dizini oluşturulduktan sonra lodash.filter’ı kurduğunuzdan emin olun. Paketi, daha sonra liste işlevinden bir arama eklerken verileri filtrelemek için kullanacağız.

npx expo init [Project Name]

# choose a template when prompted
# this example is using the 'blank' template

# after the project directory has been generated

cd [Project Name]

# install dependency
yarn add lodash.filter

FlatList Bileşenini Kullanma

React Native FlatList, genel performansı düşürmeden büyük miktarda veriden oluşan kaydırma listeleri oluşturmanın etkili bir yoludur. Büyük veri dizileri için optimize edilmiştir çünkü yalnızca ekranda görüntülenen bir dizi öğeyi oluşturur. Bir veri listesinde gezinirken, bileşen monte edildikten hemen sonra tüm verileri oluşturan ScrollView ile karşılaştırıldığında dahili durum korunmaz. Bu, ScrollView’daki tüm verilerin aygıtın belleğine eklendiği ve büyük miktarda veri işlenirken performansın düşmesine neden olabileceği anlamına gelir.

FlatList’e bir dizi veri iletmek, veri listesini nasıl görüntüleyebileceğinizdir. Bunun nasıl çalıştığını görelim. Örneğin, App.js’yi açın ve işlev bileşeninden önce aşağıdaki veri dizisini ekleyin.

const data = [
  { id: '1', title: 'First item' },
  { id: '2', title: 'Second item' },
  { id: '3', title: 'Third item' },
  { id: '4', title: 'Fourth item' }
];

Ardından, FlatList’i App.js dosyasında içe aktarın.

import { StyleSheet, Text, View, FlatList } from 'react-native';

FlatList, bir veri listesini görüntülemek için gerekli olan üç birincil props kullanacak:

data: bir liste oluşturmak için kullanılan bir veri dizisi. Dizi, eleman olarak birden çok nesneden oluşur.
keyExtractor: FlatList’e, dizinin tek tek öğeleri için benzersiz bir tanımlayıcı veya kimlik kullanmasını söyler.
renderItem: veri dizisinden tek bir öğeyi alan ve onu kullanıcı arayüzünde işleyen bir işlev.

Ardından, bu veri listesini döndürmek için Uygulama bileşenini değiştirin.

export default function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Basic FlatList Example</Text>
      <FlatList
        data={data}
        keyExtractor={item => item.id}
        renderItem={({ item }) => (
          <View style={styles.listItem}>
            <Text style={styles.listItemText}>{item.title}</Text>
          </View>
        )}
      />
    </View>
  );
}

Aşağıdaki styles nesnesini ekleyin.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f8f8',
    alignItems: 'center'
  },
  text: {
    fontSize: 20,
    color: '#101010',
    marginTop: 60,
    fontWeight: '700'
  },
  listItem: {
    marginTop: 10,
    padding: 20,
    alignItems: 'center',
    backgroundColor: '#fff',
    width: '100%'
  },
  listItemText: {
    fontSize: 18
  }
});

Şimdi emülatöre geri dönün ve veri dizisindeki tüm nesnelerin artık bir liste şeklinde görüntülendiğini göreceksiniz. FlatList’i kullanmak, organize verileri görüntülemek için minimum çaba gerektirir.

FlatList’te bir API’den veri alma

FlatList, mobil uygulamanın verileri nasıl getirdiğini umursamıyor. Önceki bölümde, bir dizi veriyle nasıl alay edileceğini ve bir liste olarak nasıl kullanılacağını öğrendik. Bu bölümde, verileri bir uzak API kaynağından alalım ve verileri görüntülemek için aynı kalıbı (önceki bölümde olduğu gibi) takip edelim.

Yan Not: Uzak bir API kaynağı için, Random User Placeholder API‘yi kullanacağım.

Bu bölümde kullanacağımız tüm gerekli bileşenleri içe aktararak başlayın. Aşağıdaki içe aktarma ifadelerini aşağıda gösterildiği gibi güncelleyin:

import React, { useState, useEffect } from 'react';
import {
  StyleSheet,
  Text,
  View,
  FlatList,
  ActivityIndicator,
  Image
} from 'react-native';

Ardından, verileri sabit olarak almak için API endpoint URL’sini tanımlayın.

const API_ENDPOINT = `https://randomuser.me/api/?seed=1&page=1&results=20``;

API endpoint HTTP isteği şimdilik ilk 20 sonucu getirecek.

React Hook useState’i kullanarak App bileşeninin içinde üç durum değişkeni tanımlayın. isLoading durum değişkeninin boolean değeri varsayılan olarak false olacaktır. Amacı, veriler API endpoint’den getirilirken bir yükleme göstergesi görüntülemektir.

Veri değişkeni varsayılan olarak boş bir diziye sahip olacaktır. Bu durum değişkenini kullanarak, FlatList bir veri listesi görüntülemek için doldurulur.

Son durum değişkeni olan error, varsayılan bir null değerine sahip olacak. Yalnızca verileri alırken bir hata olduğunda güncellenecektir.

export default function App() {
  const [isLoading, setIsLoading] = useState(false);
  const [data, setData] = useState([]);
  const [error, setError] = useState(null);

  // ...
}

Sonra, React Hook useEffect’i ve JavaScript’ten alma API’sini kullanarak, verileri API_ENDPOINT’ten alalım. App bileşeninin içindeki durum değişkenlerini tanımladıktan sonra aşağıdakileri ekleyin.

UseEffect başlatıldığında yükleme değişkeni true olarak ayarlanır. Bu değişkenin boolean değeri, yalnızca verilerin getirilmesi tamamlandığında veya bir hata olduğunda false olarak ayarlanır. Aşağıdaki setData, veri değişkenini bir veri dizisi ile güncelliyor.

export default function App() {
  // state variables defined

  useEffect(() => {
    setIsLoading(true);

    fetch(API_ENDPOINT)
      .then(response => response.json())
      .then(results => {
        setData(results);
        setIsLoading(false);
      })
      .catch(err => {
        setIsLoading(false);
        setError(err);
      });
  }, []);
  // ...
}

Ardından, her biri iki farklı senaryo için bir JSX döndüren iki if koşulu ekleyin. İlk olarak, veriler getirilirken bir yükleme göstergesi gösterilir. İkinci olarak, bir hata olduğunda bir hata mesajı görüntülenir.

export default function App() {
  // state variables defined

  // fetch data using useEffect

  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" color="#5500dc" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ fontSize: 18}}>
          Error fetching data... Check your network connection!
        </Text>
      </View>
    );
  }
  // ...
}

Ardından, kullanıcı avatarını ve API endpoint’den getirilen kullanıcının adını görüntülemek için FlatList’i güncelleyin.

export default function App() {
  // ...

  return (
    <View style={styles.container}>
      <Text style={styles.text}>Favorite Contacts</Text>
      <FlatList
        data={data}
        keyExtractor={item => item.first}
        renderItem={({ item }) => (
          <View style={styles.listItem}>
            <Image
              source={{ uri: item.picture.thumbnail }}
              style={styles.coverImage}
            />
            <View style={styles.metaInfo}>
              <Text style={styles.title}>{`${item.name.first} ${
                item.name.last
              }`}</Text>
            </View>
          </View>
        )}
      />
    </View>
  );
}

Styles nesnesini de güncellemeyi unutmayın.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f8f8',
    alignItems: 'center'
  },
  text: {
    fontSize: 20,
    color: '#101010',
    marginTop: 60,
    fontWeight: '700'
  },
  listItem: {
    marginTop: 10,
    paddingVertical: 20,
    paddingHorizontal: 20,
    backgroundColor: '#fff',
    flexDirection: 'row'
  },
  coverImage: {
    width: 100,
    height: 100,
    borderRadius: 8
  },
  metaInfo: {
    marginLeft: 10
  },
  title: {
    fontSize: 18,
    width: 200,
    padding: 10
  }
});

Bu adımdan sonra alacağımız bir FlatList kullanılarak görüntülenen kişilerin listesi aşağıdadır.

Veriler getirilirken yüklenme göstergesi burada.

Ve aşağıda, uygulamanın verileri getiremediği senaryo verilmiştir.

Search Bar Ekleme

Bu bölümde, mevcut FlatList’in en üstünde bir arama çubuğu oluşturalım. Bir arama çubuğunu görüntülemek için ListHeaderComponent adlı bir destek sağlar.

Uygulama bileşenini düzenlemeye başlamadan önce, bu adımda gerekli olan gerekli içe aktarma ifadelerini ekleyelim. React-native’den TextInput için içe aktarmayı ekleyin. Ayrıca lodash.filter dosyasını içe aktarın.

import {
  StyleSheet,
  Text,
  View,
  FlatList,
  ActivityIndicator,
  Image,
  TextInput
} from 'react-native';
import filter from 'lodash.filter';

Aşağıda gösterildiği gibi FlatList için prop ekleyin

<FlatList
  ListHeaderComponent={renderHeader}
  // ... rest of the props remain same
/>

Ardından, aşağıdaki JSX’i döndürecek renderHeader işlevini tanımlayın:

export default function App() {
  //...
  function renderHeader() {
    return (
      <View
        style={{
          backgroundColor: '#fff',
          padding: 10,
          marginVertical: 10,
          borderRadius: 20
        }}
      >
        <TextInput
          autoCapitalize="none"
          autoCorrect={false}
          clearButtonMode="always"
          value={query}
          onChangeText={queryText => handleSearch(queryText)}
          placeholder="Search"
          style={{ backgroundColor: '#fff', paddingHorizontal: 20 }}
        />
      </View>
    );
  }
// … render JSX below
}

İşte bu adımdan sonra emülatördeki çıktı.

Ardından, iki durum değişkeni daha ekleyin. İlk olarak, sorgu, veri listesinde arama yapmak için kullanıcı tarafından sağlanan herhangi bir girdiyi takip edecektir. Varsayılan bir boş dizge değerine sahiptir. İkinci olarak, verileri filtrelemek için kullanılacak olan API’den verileri tutmak için başka bir değişken ekleyin.

const [query, setQuery] = useState('');
const [fullData, setFullData] = useState([]);

Tüm Data dizisini doldurmak için side-effect olarak useEffect’i güncelleyin.

useEffect(() => {
  setIsLoading(true);

  fetch(API_ENDPOINT)
    .then(response => response.json())
    .then(response => {
      setData(response.results);

      // ADD THIS
      setFullData(response.results);

      setIsLoading(false);
    })
    .catch(err => {
      setIsLoading(false);
      setError(err);
    });
}, []);

Ardından, search bar’ı işleyecek olan handleSearch adında bir işleyici yöntemi ekleyin. Varsayılan olarak, sorgu olarak sağlanan arama terimini küçük harfe biçimlendirecektir. Kullanıcının adı, durum değişkeni fullData’dan filtrelenirken durum değişkeni verileri, doğru kullanıcıyı oluşturmak için aramadan sonra nihai sonuçları saklar.

İçerme işleyicisi yöntemi sorguyu arayacaktır. Kullanıcının adı ve soyadı ve handleSearch() ‘den küçük harfe biçimlendirilmiş sorgu olmak üzere iki parametre kabul eder.

const handleSearch = text => {
  const formattedQuery = text.toLowerCase();
  const filteredData = filter(fullData, user => {
    return contains(user, formattedQuery);
  });
  setData(filteredData);
  setQuery(text);
};

const contains = ({ name, email }, query) => {
  const { first, last } = name;

  if (first.includes(query) || last.includes(query) || email.includes(query)) {
    return true;
  }

  return false;
};

Emülatörde, bu sorguya dayalı sonuçları almak için bir arama sorgusu.

Bu makalenin odak noktası, FlatList bileşeninin sağladığı farklı donanımlara aşina olmanızı sağlamaktı.

Daha iyi sonuçlar için bir API endpoint’den veri alırken Algolia gibi güçlü bir arama sağlayıcısının kullanılması önerildiğini unutmayın.

Son olarak, hassas mantık içeren ticari React Native uygulamaları geliştiriyorsanız özel dikkat göstermeyi unutmayın. Kılavuzumuzu izleyerek onları kod hırsızlığına, kurcalanmaya ve ters mühendisliğe karşı koruyabilirsiniz.

Lütfen bize görüşlerinizi bildirmekten çekinmeyin.

Kaynak

1-https://blog.jscrambler.com/add-a-search-bar-using-hooks-and-flatlist-in-react-native/

React Native ile Mobil Uygulama Geliştirme – 10 – Drag & Drop Bileşenini Oluşturma

“React Native ile Mobil Uygulama Geliştirme” başlıklı makalelerimizden bir yenisini daha sizlerle paylaşmak istiyorum. Bu makale, bir dizi makaleden oluşacak olan serisinin 10. kısmıdır. Bir önceki makalede (Part 9), React Native’de Redux Kullanımı ve mobil projelerimizde nasıl kullanacağımızı anlattım.

React Native’de Drag & Drop bileşenini oluşturmayı ve mobil projelerimizde nasıl kullanacağımızı öğreneceğiz.

Bir sonraki React Native uygulamam için beyin fırtınası yaparken, işte ilk adım, bir Drag & Drop bileşenini oluşturabilmek için hareketi izleyen bir API olup olmadığını görmek istedim.
Neyse ki bulması çok zor olmadı: PanResponder’a sahibiz! Bununla hareketi soyut bir durum olarak izleyebilir ve kullanıcı arayüzümüzü buna göre güncelleyebiliriz.

Daha önceki makaleleri okumadıysanız, lütfen buradan başlayın.

Nasıl bir uygulama yapmak istediğimizin kafamızda netleşmesi için aşağıdaki görsele bakabilirsiniz.

Drag Bileşen Oluşturma

PanResponder API belgelerini takip etmenin biraz zor olduğunu buldum, ancak bu örnek ve animasyon kılavuzu gitmeme yardımcı oldu. PanResponder sınıfını içe aktardıktan sonra, componentWillMount() yaşam döngüsü yönteminde .create yöntemiyle başlatırsınız. Bunu constructor() da yazabilirsiniz.

import React, { Component } from "react";
import {
  StyleSheet,
  View,
  PanResponder,
  Animated
} from "react-native";

export default class Draggable extends Component {
  constructor() {
    super();
    this.state = {
      pan: new Animated.ValueXY()
    };
  }

  componentWillMount() {
    // Add a listener for the delta value change
    this._val = { x:0, y:0 }
    this.state.pan.addListener((value) => this._val = value);
    // Initialize PanResponder with move handling
    this.panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (e, gesture) => true,
      onPanResponderMove: Animated.event([
        null, { dx: this.state.pan.x, dy: this.state.pan.y }
      ])
      // adjusting delta value
      this.state.pan.setValue({ x:0, y:0})
    });
  }

  render() {
    const panStyle = {
      transform: this.state.pan.getTranslateTransform()
    }
    return (
        <Animated.View
          {...this.panResponder.panHandlers}
          style={[panStyle, styles.circle]}
        />
    );
  }
}

let CIRCLE_RADIUS = 30;
let styles = StyleSheet.create({
  circle: {
    backgroundColor: "skyblue",
    width: CIRCLE_RADIUS * 2,
    height: CIRCLE_RADIUS * 2,
    borderRadius: CIRCLE_RADIUS
  }
});

this.panResponder = PanResponder.create(), panResponder’ı başlatır ve bir referans oluşturur. Bunu <Animate.View> bileşenimizde, bir sahne grubundan geçiyormuş gibi {… this.panResponder.panHandlers} ‘ı geçirerek kullanırız.
PanResponder.create() içinde onStartShouldSetPanResponder’ı true olarak ayarladık, böylece panResponder dokunma geri bildirimlerine yanıt verecek. Sonra onPanResponderMove’a bir Animated.event iletiriz: kullanıcının hareket ettiği Animated.View circle bileşenimizin konumunu güncellemek için.
Çevremizin konumunu elde etmek için hesaplanan animasyon değerini this.state.pan.getTranslateTransform() ‘dan alıp Animated.View’a aktardığımız bir dönüştürme stili oluşturmak için kullanırız.
Son olarak, öğenin ikinci dokunuşta atlamaması için delta değerini ayarlıyoruz.
Bu noktada, kullanıcının etkileşime girebileceği sürüklenebilir bir dairemiz var:

Çemberi İlk Konumuna Geri Döndürmek

Bileşeni bıraktıklarında dairenin orijinal konumuna geri dönmesini istiyoruz. Bunu yapmak için onPanResponderRelease’i kullanarak kullanıcı arayüzüne kullanıcı bıraktığında nasıl yanıt vereceğini bildiririz.

componentWillMount() {
  ...
    this.panResponder = PanResponder.create({
      ...
      onPanResponderRelease: (e, gesture) => {
        Animated.spring(this.state.pan, {
          toValue: { x: 0, y: 0 },
          friction: 5
        }).start();
      }
    });

Dairemiz şimdi ilk konumuna geri dönüyor:

Bırakma Alanının Oluşturulması

Artık sürüklemeye sahibiz, bırakmaya ihtiyacımız var. Bırakma alanımızla başka bir bileşen oluşturuyoruz ve içinde Sürüklenebilir bileşenimizi kullanıyoruz:

import React, { Component } from "react";
import { StyleSheet, View, Text } from "react-native";
import Draggable from "./Draggable";

export default class Screen extends Component {
  render() {
    return (
      <View style={styles.mainContainer}>
        <View style={styles.dropZone}>
          <Text style={styles.text}>Drop them here!</Text>
        </View>
        <View style={styles.ballContainer} />
        <View style={styles.row}>
          <Draggable />
          <Draggable />
          <Draggable />
          <Draggable />
          <Draggable />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  mainContainer: {
    flex: 1
  },
  ballContainer: {
    height:200
  },
  row: {
    flexDirection: "row"
  },  
  dropZone: {
    height: 200,
    backgroundColor: "#00334d"
  },
  text: {
    marginTop: 25,
    marginLeft: 5,
    marginRight: 5,
    textAlign: "center",
    color: "#fff",
    fontSize: 25,
    fontWeight: "bold"
  }
});

Ardından Draggable, onPanResponderRelease işleyicisine biraz mantık ekliyoruz ve bırakma alanında olup olmadığımızı belirlemek için bir işlev ekliyoruz. Not: Burada ekrandaki bırakma bölgesinin konumu hakkında varsayımlarda bulunuyoruz.

...
  constructor()
    super.props();
    this.state = {
      showDraggable: true,
      dropAreaValues: null,
      pan: new Animated.ValueXY(),
      opacity: new Animated.Value(1)
    };
  }
  componentWillMount() {
    ...
    this.panResponder = PanResponder.create({
      ...
      onPanResponderRelease: (e, gesture) => {
        if (this.isDropArea(gesture)) {
          Animated.timing(this.state.opacity, {
          toValue: 0,
          duration: 1000
        }).start(() =>
          this.setState({
             showDraggable: false
          })
        );
      } else {
        Animated.spring(this.state.pan, {
          toValue: { x: 0, y: 0 },
          friction: 5
        }).start();
      }
    }
  isDropArea(gesture) {
    return gesture.moveY < 200;
  }

Uygulamayı çalıştırdığımızda aşağıdaki gibi bir görüntü ortaya çıkar.

PanResponder API’si ilk başta benim için biraz korkutucuydu, ancak şimdi bu, herkesin uygulamalarında kullanıcı deneyimini daha iyi hale getirmek için kullanabileceği harika bir API olduğunu düşünüyorum.
Umarım bu makale, sorularınız veya endişeleriniz olan yorumlarda size ulaşmanıza yardımcı olmuştur. Okuduğunuz için teşekkürler ve iyi kodlamalar!

Lütfen bize görüşlerinizi bildirmekten çekinmeyin.

Kaynak

1-https://blog.reactnativecoach.com/creating-draggable-component-with-react-native-132d30c27cb0

Material Motion Part 2: Container Transform

Yazdığınız uygulamanın kullanıcı sayısının artmasını sağlayan en önemli konulardan biri, kullanıcı deneyimi yüksek ve dikkat çekici tasarıma sahip olmasıdır.

Android uygulama da UI öğeleri arasındaki geçişleri farklı animasyon görünümleri ile sağlayan çeşitli Material motion özellikleri bulunmaktadır. Bu özelliklerden biri olan Shared Axis hakkındaki örnek projeyi  “Material Motion Part 1: Shared Axis” adlı makalemden ulaşabilirsiniz.

Bu makale serisinde, Android uygulamada iki farklı arayüz elementinin birbirleri arasında animasyonlu bir şekilde geçişini sağlayan Material motion Container Transform özelliğini örnekleyeceğim.

Container Transform

Bir arayüz elementinin başka bir arayüz elementine animasyonlu bir şekilde geçişini sağlayan özelliktir. Böylelikle kullanıcının mobil tasarım öğeleriyle etkileşime girmesini sağlamış oluruz. Bu durum projenin uzun vadede kullana bilirliğini artırır.

Makalemizde anlatacağım örnek uygulamanın görüntüsü;

Projemin kodlarına github linkinden hızlıca ulaşabilirsiniz.

Bu örneği uygulayabilmek için işlemleri adım adım yapalım.

1-Gerekli Kütüphanelerin Yüklenmesi

Projemin app dizinin altındaki build.gradle dosyasını açıyoruz. Dependencies kod bloklarının arasına aşağıdaki kodları yerleştirerek navigation ve material kütüphanelerini yüklüyoruz.

implementation('androidx.navigation:navigation-fragment-ktx:2.3.1')
implementation('androidx.navigation:navigation-ui-ktx:2.3.1')
implementation('com.google.android.material:material:1.3.0-alpha03')

2- Kotlin ve tasarım kodları

İlk önce Note sınıfından bahsetmek istiyorum. Tüm not başlıklarını, içeriklerini ve not kutularının renklerini atadığım ve bu değerleri bir listOf array yapısında barındırdığım sınıfdır.

import androidx.annotation.ColorRes
data class Note(
    val id: Int,
    val title: String,
    val body: String,
    @ColorRes val colorRes: Int
)

val note1 = Note(
    id = 1,
    title = "ToDo 1",
    body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus scelerisque placerat nisl, nec semper felis ullamcorper vel. Nullam egestas ante nec tortor egestas mattis. Duis ut diam nec nibh sodales commodo at at diam. Nunc tempor eu lectus ut feugiat. Etiam eget ullamcorper est, at scelerisque lectus. Aliquam erat volutpat. Maecenas est urna, vestibulum non eros non, dignissim feugiat mi. Cras sit amet ex hendrerit, accumsan dolor in, bibendum erat. Maecenas ullamcorper ut risus eget congue. Vestibulum aliquam ipsum ut turpis efficitur, vel malesuada neque aliquam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean egestas justo id rutrum blandit. Vestibulum id orci libero. Aliquam pharetra sed mauris ac vehicula.",
    colorRes = R.color.blue300
)

val note2 = Note(
    id = 2,
    title = "ToDo 2",
    body = "Fusce hendrerit enim in eros congue, sed pharetra libero tempus. Integer accumsan euismod nibh non vestibulum. Curabitur finibus imperdiet nunc vel ornare. Ut maximus fringilla sapien in viverra. Aenean a nulla feugiat, hendrerit risus et, congue erat. Ut venenatis lorem sit amet volutpat sollicitudin. Donec ac lorem auctor sem mattis faucibus non ac ante. Phasellus id sem non ante bibendum porta non in tellus. Etiam pellentesque porta luctus.",
    colorRes = R.color.amber300
)

val note3 = Note(
    id = 3,
    title = "ToDo 3",
    body = "Praesent interdum dictum magna quis pretium. Suspendisse at cursus ante, id rutrum nunc. Nullam at lacinia nibh, nec gravida lectus. Quisque maximus vulputate leo, et sollicitudin turpis luctus pellentesque. Nullam vehicula sagittis magna, consectetur congue neque ullamcorper vel. Praesent sed vulputate nunc. Ut ligula lorem, lobortis tristique interdum at, mollis a dolor. Ut sed fringilla urna, id suscipit justo. Praesent ut tortor pharetra, laoreet metus non, iaculis mi. Duis vel lacus fermentum, porta neque id, ultricies arcu. Nunc interdum, est ac sodales tristique, elit urna feugiat dui, quis venenatis ante lorem ullamcorper mi. Nulla id condimentum lorem. Nunc ac scelerisque felis. Nam semper, mi et ultrices rutrum, neque odio molestie tortor, ac iaculis ante arcu eu elit. Phasellus imperdiet tortor quis aliquam ornare.",
    colorRes = R.color.green300
)

val notes = listOf(note1, note2, note3)

 

MainActivity sınıfımızda ise, notların grid şeklinde gösterilmesi sağlayan ve not kutucuklarına tıklandığı anda yapılan geçiş animasyonunu tetiklemesini yaptık. Detaylı açıklamalar kodlar arasında bulunmaktadır.

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.MenuItem
import androidx.core.app.ActivityOptionsCompat
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.card.MaterialCardView
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setRecyclerView()
    }
    private fun setRecyclerView() {
        //Note data class da tanımlanan başlık ve içerikler NotesAdapter sınıfına gönderiliyor
        val adapter = NotesAdapter(notes)
        //Not kutularına tıklanma eventi
        adapter.noteClickListener = object : NotesAdapter.NoteClickListener {
            override fun onNoteClick(id: Int, noteCard: MaterialCardView) {

                val intent = Intent(this@MainActivity, NoteDetailActivity::class.java)
                //Not kutusuna tıkladığı anda yapılan geçiş animasyonunu tetiklediğimiz bölüm
                val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                    this@MainActivity, noteCard, id.toString()
                )
                //İlgili içeriğin id'si NoteDetailActivity sınıfına gönderiliyor
                intent.putExtra("noteId", id)
                startActivity(intent, options.toBundle())
            }
        }
        //Notları grid şeklinde gösterilmesi sağlayan bölüm
        recycler_view.layoutManager = GridLayoutManager(this, 2)
        recycler_view.adapter = adapter
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                onBackPressed()
                true
            }
            else -> {
                true
            }
        }
    }

    override fun onBackPressed() {
        finish()
    }
}

Notların grid şeklinde gösterilmesi için activity_main xml dosyasında RecyclerView arayüz elementi kullanıldı.

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        style="@style/Widget.MaterialComponents.AppBarLayout.Surface"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.textview.MaterialTextView
                style="?attr/textAppearanceHeadline6"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:textColor="@color/material_on_surface_emphasis_medium"
                android:text="My notes" />
        </com.google.android.material.appbar.MaterialToolbar>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

NotesAdapter sınıfının işlevi, MainActivity sınıfında kullanılan RecyclerView içinde tüm not item’larını oluşturmaktır. Bu item kutucuklarında da Note sınıfından gelen notlarla ilgili içerikleri item_note xml dosyasına aktarmasıyla tasarımda gösterilmesidir.

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.smality.materialcontainertransform.databinding.ItemNoteBinding
import com.google.android.material.card.MaterialCardView

class NotesAdapter(private val items: List<Note>) : RecyclerView.Adapter<NotesAdapter.ViewHolder>() {
    lateinit var noteClickListener: NoteClickListener

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        ViewHolder(
            ItemNoteBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position])

    inner class ViewHolder(val binding: ItemNoteBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(note: Note) {
            binding.note = note
            binding.noteCard.transitionName = note.id.toString()
            binding.noteCard.setOnClickListener {
                noteClickListener.onNoteClick(note.id, binding.noteCard)
            }
        }
    }

    interface NoteClickListener {
        fun onNoteClick(id: Int, noteCard: MaterialCardView)
    }
}

item_note xml dosyasında MaterialCardView arayüz elementini notları kutucuk şeklinde göstermek için kullandık. MaterialCardView içinde de MaterialTextView arayüz elementine not başlık ve içeriklerini atadık.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="note"
            type="com.smality.materialcontainertransform.Note" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.card.MaterialCardView
            android:id="@+id/note_card"
            android:layout_gravity="center"
            android:clickable="true"
            android:focusable="true"
            android:layout_margin="8dp"
            app:cardBackgroundColor="@{context.resources.getColor(note.colorRes)}"
            android:layout_width="match_parent"
            android:layout_height="200dp">
            <LinearLayout
                android:padding="16dp"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <com.google.android.material.textview.MaterialTextView
                    style="?attr/textAppearanceBody1"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@{note.title}" />

                <com.google.android.material.textview.MaterialTextView
                    style="?attr/textAppearanceCaption"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:ellipsize="end"
                    android:maxLines="10"
                    android:layout_marginTop="8dp"
                    android:text="@{note.body}" />
            </LinearLayout>
        </com.google.android.material.card.MaterialCardView>
    </FrameLayout>
</layout>

Son olarak, NoteDetailActivity sınıfını kullandık. Bu sınıf, not kutularına tıklandığında notların detaylandırının gösterildiği sayfadır. Diğer bir yandan bu sınıfta, RecyclerView nesnesinden MaterialTextView nesnesine geciş anının süresinin belirlendiği kod sayfasıdır. Detaylı açıklamalar kodlar arasında bulunmaktadır.

import android.os.Bundle
import android.view.MenuItem
import android.view.Window
import androidx.appcompat.app.AppCompatActivity
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.transition.platform.MaterialContainerTransform
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import com.smality.materialcontainertransform.databinding.NoteDetailActivityBinding

class NoteDetailActivity : AppCompatActivity() {
    private lateinit var binding: NoteDetailActivityBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
        binding = NoteDetailActivityBinding.inflate(layoutInflater)
        //MainActivity sınıfından gelen noteId değerini aldık
        val noteId = intent.getIntExtra("noteId", 0)
        //Id bilgisine göre Note data class daki not içeriğini alıp, note nesnesine atadık
        val note = notes.find { it.id == noteId }
        binding.note = note
        //
        binding.coordinator.transitionName = noteId.toString()
        setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())
        //buildContainerTransform metodunda oluturulan özelliklerin geciş esnasında uygulanması için
        //yapılan atama
        window.sharedElementEnterTransition = buildContainerTransform()
        window.sharedElementReturnTransition = buildContainerTransform()
        setContentView(binding.root)
        super.onCreate(savedInstanceState)
        setSupportActionBar(binding.toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        supportActionBar?.setDisplayShowTitleEnabled(false)
    }
    //RecyclerView nesnesinden MaterialTextView nesnesine geciş anının süresini belirkeyen metod
    private fun buildContainerTransform() =
        MaterialContainerTransform().apply {
            addTarget(binding.coordinator)
            duration = 300
            //geçiş anındaki animasyon hızını ayarlayan metod
            interpolator = FastOutSlowInInterpolator()
            fadeMode = MaterialContainerTransform.FADE_MODE_IN
        }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                onBackPressed()
                true
            }
            else -> {
                true
            }
        }
    }
}

note_detail_activity xml dosyasında ise, MaterialTextView arayüz elementi kullanılarak seçilen notun detaylarının gösterildi.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="note"
            type="com.smality.materialcontainertransform.Note" />
    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/coordinator"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@{context.resources.getColor(note.colorRes)}">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true">

            <com.google.android.material.textview.MaterialTextView
                style="?attr/textAppearanceHeadline6"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:text="@{note.title}"
                android:textColor="@color/material_on_surface_emphasis_medium" />
        </com.google.android.material.appbar.MaterialToolbar>

        <androidx.core.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="16dp"
            android:layout_marginTop="?actionBarSize">

            <com.google.android.material.textview.MaterialTextView
                style="?attr/textAppearanceBody2"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:ellipsize="end"
                android:maxLines="10"
                android:layout_marginTop="8dp"
                android:text="@{note.body}" />
        </androidx.core.widget.NestedScrollView>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

Dilerseniz projemin kodlarına github linkinden hızlıca ulaşabilirsiniz.