TextField안의 상단에 prefix 넣기

custom_form_field.dart

import 'package:flutter/material.dart';

import '../constants.dart';

class CustomFormField extends StatelessWidget {
  const CustomFormField({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        TextFormField(
            textAlignVertical: TextAlignVertical.bottom,
            decoration: InputDecoration(
                contentPadding: EdgeInsets.only(top: 30, left: 20, bottom: 10),
                hintText: "근처 추천 장소",
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(10),
                ))),
        Positioned(
          left: 20,
          top: 8,
          child: Text(
            "위치",
            style: kTextFormBody2Style,
          ),
        ),
      ],
    );
  }
}

설명

Stack안에 

TextFormField를 디자인하고 

position을 통해 prefix를 디자인 해준다.

 

728x90

키보드 높이 만큼 페이지 이동(동적스크롤링)

pages / login_page.dart

CustomTextFormField에 scrollAnimate 함수 넘겨줌

// statefulwidget으로 변경
class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  //scrollController 생성
  late ScrollController scrollController;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //초기화
    scrollController = new ScrollController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: ListView(
          controller: scrollController,
          children: [
            SizedBox(height: 100),
            Logo("Login"),
            SizedBox(height: 50),
            buildLoginForm(),
            SizedBox(height: 50),
            TextButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  Navigator.pushNamed(context, "/home");
                }
              },
              child: Text("Login"),
            ),
            SizedBox(height: 10),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text("New Here? "),
                GestureDetector(
                  onTap: () {
                    print("Join 클릭");
                  },
                  child: Text(
                    "Join",
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      decoration: TextDecoration.underline,
                    ),
                  ),
                )
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget buildLoginForm() {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          CustomTextFormField("Email", scrollAnimate),
          CustomTextFormField("Password", scrollAnimate),
        ],
      ),
    );
  }

  void scrollAnimate() {
    print("탭 클릭됨");
    // 6초 후에 실행
    Future.delayed(Duration(milliseconds: 600), () {
      // MediaQuery.of(context).viewInsets.bottom 하단 inset(사용못하는영역)크기 리턴
      // 사용못하는 영역만큼 1초 동안 easeIn으로 이동
      scrollController.animateTo(MediaQuery.of(context).viewInsets.bottom,
          duration: Duration(milliseconds: 100), curve: Curves.easeIn);
    });
  }
}

components / custom_text_form_field.dart

import 'package:flutter/material.dart';

class CustomTextFormField extends StatelessWidget {
  final String subtitle;
  //scrollAnimate 함수 받아오기
  final Function scrollAnimate;
  const CustomTextFormField(this.subtitle, this.scrollAnimate, {Key? key})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(subtitle),
        SizedBox(height: 5),
        TextFormField(
          onTap: () {
          // text field tab할때 실행
            scrollAnimate();
          },
          validator: (value) =>
              value!.isEmpty ? "Please Enter some text" : null,
          decoration: InputDecoration(
            hintText: "Enter $subtitle",
            focusedBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(20),
            ),
            enabledBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(20),
            ),
            errorBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(20),
            ),
            focusedErrorBorder: OutlineInputBorder(
              borderRadius: BorderRadius.circular(20),
            ),
          ),
        ),
        SizedBox(height: 10),
      ],
    );
  }
}

 

728x90

Tab menu 구현

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.white,
        appBarTheme: AppBarTheme(
          iconTheme: IconThemeData(color: kColor1),
        ),
      ),
      home: Profile(),
    );
  }
}

// TabController 객체를 멤버로 만들어서 상태를 유지하기 때문에 StatefulWidget 클래스 사용
class Profile extends StatefulWidget {
  @override
  _ProfileState createState() => _ProfileState();
}

// SingleTickerProviderStateMixin 클래스는 애니메이션을 처리하기 위한 헬퍼 클래스
// 상속에 포함시키지 않으면 탭바 컨트롤러를 생성할 수 없다.
// mixin은 다중 상속에서 코드를 재사용하기 위한 한 가지 방법으로 with 키워드와 함께 사용
class _ProfileState extends State<Profile> with SingleTickerProviderStateMixin {
//tab controller 생성
  late TabController _tabController;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //tabcontroller 초기화
    //vsync에 this를 넣기위해 initState안에서 초기화
    _tabController = new TabController(length: 2, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(15.0),
        child: Column(
          children: [
            TabBar(
              controller: _tabController,
              tabs: [
              	//첫번째 tab menu
                Tab(icon: Icon(Icons.directions_car)),
              	//두번째 tab menu               
                Tab(icon: Icon(Icons.directions_transit)),
              ],
            ),
            Expanded(
              child: TabBarView(
                controller: _tabController,
                //tab content
                children: [
                  // 첫번째 tab menu content
                  GridView.builder(
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 3,
                      crossAxisSpacing: 10,
                    ),
                    itemBuilder: (context, index) {
                      return Image.network(
                          "https://picsum.photos/id/${index + 1}/200/200");
                    },
                  ),
                  // 두번째 tab menu content
                  Image.asset("assets/tab1.jpeg"),
                ],
              ),
            )
          ],
        ),
      ),
    );
  }
}

참고

https://www.youtube.com/watch?v=8B2uPE4lSeE&list=PL93mKxaRDidG6CYF5M_RbOXGkAGLtM_Ct&index=8 

 

728x90

하단 아이콘 클릭시 아이콘 배경 색 및 상단의 이미지 변경

main.dart

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: kPrimaryColor,
        scaffoldBackgroundColor: kPrimaryColor,
      ),
      home: ShoppingCartPage(),
    );
  }
}

class ShoppingCartPage extends StatefulWidget {
  const ShoppingCartPage({Key? key}) : super(key: key);

  @override
  _ShoppingCartPageState createState() => _ShoppingCartPageState();
}

class _ShoppingCartPageState extends State<ShoppingCartPage> {
  int selectedIconNum = 0;
  List<String> selectPic = [
    "assets/p1.jpeg",
    "assets/p2.jpeg",
    "assets/p3.jpeg",
    "assets/p4.jpeg",
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          _productPic(),
          _productSelector(),
        ],
      ),
    );
  Widget _productSelector() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
        //버튼에 자신의 번호, 선택된 번호, 아이콘, setState를 넘겨줌
          ProductIcon(0, selectedIconNum, Icons.directions_bike, changeIcon),
          ProductIcon(1, selectedIconNum, Icons.motorcycle, changeIcon),
          ProductIcon(
              2, selectedIconNum, CupertinoIcons.car_detailed, changeIcon),
          ProductIcon(3, selectedIconNum, CupertinoIcons.airplane, changeIcon),
        ],
      ),
    );
  }

  Widget _productPic() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: AspectRatio(
        aspectRatio: 5 / 3,
        child: Image.asset(
        //setState로 현재 선택된 num을 받아와 리스트에서 이미지를 찾음
          selectPic[selectedIconNum],
          fit: BoxFit.cover,
        ),
      ),
    );
  }

  void changeIcon(int num) {
  //components의 버튼에 함수를 넘겨 main에서 state를 변경
    setState(() {
      selectedIconNum = num;
    });
  }
}

components/product_icon.dart

import 'package:flutter/material.dart';

import '../constants.dart';

class ProductIcon extends StatelessWidget {
  final int productNum;
  final int selectedIconNum;
  final IconData mIcon;
  final Function changeIcon;

  const ProductIcon(
  //main에서 받아와야하는 값 정의
    this.productNum,
    this.selectedIconNum,
    this.mIcon,
    this.changeIcon, {
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 70,
      height: 70,
      decoration: BoxDecoration(
      // 선택된 버튼, 선택안된 버튼 배경색 삼항 연산자로 정의
        color: productNum == selectedIconNum ? kAccentColor : kSecondaryColor,
        borderRadius: BorderRadius.circular(20),
      ),
      child: IconButton(
        onPressed: () {
        //받아온 change함수에 선택된 num 보내서 main에서 state 변경
          changeIcon(productNum);
        },
        icon: Icon(
          mIcon,
          color: Colors.black,
        ),
      ),
    );
  }
}

참고

https://www.youtube.com/watch?v=adA3vPVlh4Q&list=PL93mKxaRDidG6CYF5M_RbOXGkAGLtM_Ct&index=19

 

728x90

상세페이지의 update,delete를 위한 소유권 확인

  1. 상세페이지의 pk 가져오기

urls.py

urlpatterns = [
    path(
        "update/<int:pk>",
        ArticleUpdateView.as_view(),
        name="update",
    ),
    path(
        "delete/<int:pk>",
        ArticleDeleteView.as_view(),
        name="delete",
    ),
]
  1. decorator 생성시 글 작성자와 로그인한 유저가 동일한지 확인

decorators.py

from django.http import HttpResponseForbidden

from articleapp.models import Article


def article_ownership_required(func):
    def decorated(request, *args, **kwargs):
        article = Article.objects.get(pk=kwargs["pk"])
        if not article.writer == request.user:
            return HttpResponseForbidden()
        return func(request, *args, **kwargs)

    return decorated
  1. view에서 실제로 적용하기

views.py

@method_decorator(article_ownership_required, "get")
@method_decorator(article_ownership_required, "post")
class ArticleUpdateView(UpdateView):
    model = Article
    form_class = ArticleCreationForm
    context_object_name = "target_article"
    template_name = "articleapp/update.html"

    def get_success_url(self):
        return reverse("articleapp:detail", kwargs={"pk": self.object.pk})
  1. 리스트형태로 decorator 가져오기 가능

views.py


# decorators.py에서 사용할 decorator를 가져와 리스트에 삽입
has_owership = [login_required, article_ownership_required]

@method_decorator(has_owership, "get")
@method_decorator(has_owership, "post")
class ArticleUpdateView(UpdateView):
    model = Article
    form_class = ArticleCreationForm
    context_object_name = "target_article"
    template_name = "articleapp/update.html"

    def get_success_url(self):
        return reverse("articleapp:detail", kwargs={"pk": self.object.pk})
728x90

React Slick 설치

npm i react-slick

아래의 css 도 설치해 줍니다.

npm install slick-carousel

기본 Slider 생성

css 를 불러와야합니다.

import React from "react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";

export default function SimpleSlider() {
  var settings = {
    dots: true,
    infinite: true,
    speed: 500,
    slidesToShow: 1,
    slidesToScroll: 1
  };
  return (
    <Slider {...settings}>
      <div>
        <h3>1</h3>
      </div>
      <div>
        <h3>2</h3>
      </div>
      <div>
        <h3>3</h3>
      </div>
      <div>
        <h3>4</h3>
      </div>
      <div>
        <h3>5</h3>
      </div>
      <div>
        <h3>6</h3>
      </div>
    </Slider>
  );
}

현재 선택된 슬라이드 index 값 가져오기

useState를 활용하여 현재 슬라이드의 index를 넣어줍니다.

react-slick의 api문서의 beforeChange를 통해 받아올 수 있습니다.

beforeChange
Type: func
Default: null
Description: Index change callback. `(oldIndex, newIndex) => ...`
 const [slideIndex, setSlideIndex] = useState(0);
  const settings = {
    dots: true,
    infinite: true,
    speed: 500,
    slidesToShow: 1,
    slidesToScroll: 1
    beforeChange: (current, next) => {
      return setSlideIndex(current);
    },
  };

슬라이드 컴포넌트별로 현재 선택된 슬라이드인지 비교합니다.

슬라이드는 0부터 시작합니다.

<Slider {...settings}>
      <Slide border={0==slideIndex}>
        <h3>1</h3>
      </div>
      <Slide border={1==slideIndex}>
        <h3>2</h3>
      </div>
      <Slide border={2==slideIndex}>
        <h3>3</h3>
      </div>
      <Slide border={3==slideIndex}>
        <h3>4</h3>
      </div>
      <Slide border={4==slideIndex}>
        <h3>5</h3>
      </div>
      <Slide border={5==slideIndex}>
        <h3>6</h3>
      </div>
    </Slider>

현재 선택된 슬라이드가 맞으면 true 아니면 false를 porps.border로 전달해줍니다.

전달 받은 props.border가 true 이면 빨간색 테두리 아니면 none으로 스타일링 해줍니다.

const Slide = styled.div`
	border:${(props)=> (props.border ? "1px solid red" : "none")}
`;

 

728x90

+ Recent posts