달력

5

« 2024/5 »

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
반응형

일전에 이미지를 특정 영역에서 크기와 위치가 변경될 수 있는 기능을 만들어야 하는 일이 있었다.
당시 pub.dev에서 해당 기능이 구현되어 있는 패키지를 찾아보았지만 딱 맞는 것이 없었다.
 
결국 제스처로 크기(scale)와 x축 y축을 통해 위치를 자유롭게 조정할 수 있는 matrix_gesture_detector를 발견해서
아래 소스를 작성하여 원하는 기능을 구현할 수 있었다.
 
1. yaml을 작성한다.

path_provider: 2.0.1
matrix_gesture_detector: 0.1.0

 
2. moveImage.dart를 작성한다.

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
import 'package:path_provider/path_provider.dart';

class moveImage extends StatefulWidget {
  String path;
  String fileType;
  double width;
  double height;

  @override
  _moveImageState createState() => _moveImageState();

  moveImage({
    @required this.path,
    this.fileType = "storage",
    this.width = 450,
    this.height = 250,
    Key key})
      : super(key: key);
}

class _moveImageState extends State<moveImage> {
  Map<String, dynamic> item = {};
  
  @override
  void initState() {
    super.initState();
    setImageFileInfo();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        width: widget.width,
        height: widget.height,
        child: moveImage(this.item)
    );
  }

  Widget moveImage(item) {
    return new FutureBuilder<File>(
        future: item["fileType"] == "assets" ? getImageFileFromAssets(item["path"]) : getFile(item["path"]),
        builder: (context, snapshot) {
          if (snapshot.data == null || item["matrix"] == null) {
            return Container();
          } else {
            return MatrixGestureDetector(
                shouldRotate: false,
                shouldScale: true,
                onMatrixUpdate: (m, tm, sm, rm) {
                  setState(() {
                    var maxWidth = widget.width;
                    var maxHeight = widget.height;
                    var width = item["width"];
                    var height = item["height"];

                    Matrix4 matrix = Matrix4.identity();
                    matrix = MatrixGestureDetector.compose(item["matrix"], tm, sm, rm);
                    Offset translation = MatrixGestureDetector.decomposeToValues(matrix).translation;
                    Size changeSize = MatrixUtils.transformRect(matrix, item["transformKey"].currentContext.findRenderObject().paintBounds).size;

                    var initialScale = item["initialScale"];
                    var changeWidth = changeSize.width;
                    var changeHeight = changeSize.height;

                    var chkRatio = changeHeight/maxHeight;

                    if (initialScale / 2.0 <= chkRatio && chkRatio <= initialScale + 2.0) {
                      Offset currentTranslation = MatrixGestureDetector.decomposeToValues(item["matrix"]).translation;

                      var fullHeight = height * maxWidth / width;
                      var fullChangeWidth = width * changeHeight / height;
                      var fullChangeHeight = height * changeWidth / width;
                      if (fullHeight >= maxHeight) {
                        if (translation.dy > 0 || translation.dx > 0
                            || changeHeight + translation.dy < maxHeight || fullChangeWidth + translation.dx < maxWidth) {
                          var dy = translation.dy, dx = translation.dx;

                          if (changeHeight >= maxHeight) {
                            if (translation.dy > 0) {
                              dy = 0.0;
                            }
                            if (changeHeight + translation.dy < maxHeight) {
                              if (changeHeight > maxHeight) {
                                dy = maxHeight - changeHeight;
                              } else {
                                dy = currentTranslation.dy;
                              }
                            }
                          } else {
                            if (maxHeight < changeHeight + translation.dy) {
                              dy = maxHeight - changeHeight;
                            } else if (0.0 > currentTranslation.dy + translation.dy) {
                              dy = 0.0;
                            } else {
                              dy = translation.dy;
                            }
                          }

                          if (fullChangeWidth >= maxWidth) {
                            if (translation.dx > 0){
                              dx = 0.0;
                            }
                            if (fullChangeWidth + translation.dx < maxWidth) {
                              if (fullChangeWidth > maxWidth) {
                                dx = maxWidth - fullChangeWidth;
                              } else {
                                dx = currentTranslation.dx;
                              }
                            }
                          } else {
                            if (maxWidth < fullChangeWidth + translation.dx) {
                              dx = maxWidth - fullChangeWidth;
                            } else if (0.0 > currentTranslation.dx + translation.dx) {
                              dx = 0.0;
                            } else {
                              dx = translation.dx;
                            }
                          }

                          tm = translateMatrix(new Offset(dx - currentTranslation.dx, dy - currentTranslation.dy));
                          matrix = MatrixGestureDetector.compose(item["matrix"], tm, sm, rm);
                        }
                      } else {
                        if (translation.dy > 0 || translation.dx > 0
                            || fullChangeHeight + translation.dy < maxHeight || changeWidth + translation.dx < maxWidth) {
                          var dy = translation.dy, dx = translation.dx;

                          if (fullChangeHeight >= maxHeight) {
                            if (translation.dy > 0) {
                              dy = 0.0;
                            }
                            if (fullChangeHeight + translation.dy < maxHeight) {
                              if (fullChangeHeight > maxHeight) {
                                dy = maxHeight - fullChangeHeight;
                              } else {
                                dy = currentTranslation.dy;
                              }
                            }
                          } else {
                            if (maxHeight < fullChangeHeight + translation.dy) {
                              dy = maxHeight - fullChangeHeight;
                            } else if (0.0 > currentTranslation.dy + translation.dy) {
                              dy = 0.0;
                            } else {
                              dy = translation.dy;
                            }
                          }

                          if (changeWidth >= maxWidth) {
                            if (translation.dx > 0){
                              dx = 0.0;
                            }
                            if (changeWidth + translation.dx < maxWidth) {
                              if (changeWidth > maxWidth) {
                                dx = maxWidth - changeWidth;
                              } else {
                                dx = currentTranslation.dx;
                              }
                            }
                          } else {
                            if (maxWidth < changeWidth + translation.dx) {
                              dx = maxWidth - changeWidth;
                            } else if (0.0 > currentTranslation.dx + translation.dx) {
                              dx = 0.0;
                            } else {
                              dx = translation.dx;
                            }
                          }

                          tm = translateMatrix(new Offset(dx - currentTranslation.dx, dy - currentTranslation.dy));
                          matrix = MatrixGestureDetector.compose(item["matrix"], tm, sm, rm);
                        }
                      }
                      item["matrix"] = matrix;
                    }
                  });
                }, child: new Transform( key: item["transformKey"], transform: item["matrix"],
                child: new Image.file(snapshot.data, fit:BoxFit.contain, alignment: Alignment.topLeft,)
              )
            );
          }
        });
  }

  Matrix4 translateMatrix(Offset translation) {
    var dx = translation.dx;
    var dy = translation.dy;

    return Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
  }

  Future setImageFileInfo() async {
    this.item["path"] = widget.path;
    this.item["fileType"] = widget.fileType;

    if (this.item["transformKey"] == null) {
      this.item["transformKey"] = new GlobalKey();
    }

    File image;
    if (this.item["fileType"] == "assets") {
      image = await getImageFileFromAssets(this.item["path"]);
    } else {
      image = new File(this.item["path"]);
    }
    var decodedImage = await decodeImageFromList(image.readAsBytesSync());
    this.item["width"] = decodedImage.width.toDouble();
    this.item["height"] = decodedImage.height.toDouble();

    var maxWidth = widget.width;
    var maxHeight = widget.height;
    var width = this.item["width"];
    var height = this.item["height"];

    var widthRatio = (maxWidth/width);
    var heightRatio = (maxHeight/height);
    var initialScale = (heightRatio/widthRatio);

    var fullHeight = height * maxWidth / width;
    if (fullHeight >= maxHeight) {
      initialScale = (widthRatio/heightRatio);
    }

    setState(() {
      this.item["initialScale"] = initialScale;
      this.item["matrix"] = Matrix4.identity().scaled(initialScale);
    });
  }
}

Future<File> getImageFileFromAssets(String path) async {
  final byteData = await rootBundle.load('assets/$path');

  String tmpPath = '${(await getTemporaryDirectory()).path}/$path';

  final file = File(tmpPath);
  await file.create(recursive: true);
  await file.writeAsBytes(byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes));

  return file;
}

Future<File> getFile(String path) async {
  return File(path);
}

 
3. 작성한 moveImage.dart를 widget으로 사용할 화면의 dart 소스에서 불러온다.

moveImage(
  path: "test.jpeg",
  fileType: "assets",
),

 
위 예시는 assets에 있는 이미지 기준으로 작성되어 있으며
휴대폰에 있는 사진첩에서 이미지를 가져온다면(권한 관련 처리도 필수!)
fileType를 없애고 path에 이미지의 실제 경로(사진 선택 기능을 통해 알게 된 경로)를 넣어주면 된다.
 
 

 
 
참고 : https://pub.dev/packages/matrix_gesture_detector

반응형
:
Posted by 리샤씨
반응형

최근 Flutter TextField에서 숫자 입력 시 천 단위 마다 콤마를 표시되도록 해야하는 일이 생겼는데

이게 생각보다 간단하지 않았다. 

일단 숫자 입력만 되도록 제한하고 천 단위 마다 콤마 표시하는 것 자체는 크게 어렵지 않았는데,

콤마를 표시하면 그것도 자리 차지를 하게 되어 중간에서 입력/삭제 후 자연스러운 커서 위치를 잡기가 어려웠지만

규칙을 찾아 수식을 작성해서 해결했다.

 

완성된 코드는 다음과 같다.

import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

class ThousandCommaTextField extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xffffffff),
      body: SafeArea(
        child: Container(
          padding: EdgeInsets.all(10),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextField(
                keyboardType: TextInputType.number,
                textAlign: TextAlign.right,
                inputFormatters: <TextInputFormatter>[
                  FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
                  FilteringTextInputFormatter.digitsOnly,
                  ThousandCommaInputFormatter()
                ],
              )
            ],
          )
        )
      ),
    );
  }
}

String thousandComma(dynamic val, {String defaultVal = ""}) {
  NumberFormat numberFormat = NumberFormat('#,###', "ko_KR");
  if (val != null && val != "") {
    if (val is int || val is double) {
      return numberFormat.format(val);
    } else if (double.tryParse(val) != null) {
      return numberFormat.format(double.parse(val));
    } else if (int.tryParse(val) != null) {
      return numberFormat.format(int.parse(val));
    }
  }

  return defaultVal;
}

class ThousandCommaInputFormatter extends TextInputFormatter {
  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
    String oldNum = oldValue.text.replaceAll(",", "");
    String newNum = newValue.text.replaceAll(",", "");

    String newText = thousandComma(newValue.text, defaultVal: "0");

    // 다음 커서 위치 잡기
    int selectionIndex = ((newValue.text.length -1) / 3).floor() // 전체 ,(comma)의 수
                        - (((newValue.text.length - newValue.selection.end)-1) / 3).floor() //현재 커서 위치를 기준으로 오른쪽에 있는 ,(comma)의 수
                        + newValue.selection.end;

    if (selectionIndex > newText.length) {
      selectionIndex = newText.length;
    }

    if (oldNum == newNum) {//,(comma) 뒤에 커서놓고 삭제 시
      //1,234,567에서 4,뒤(커서 위치 6)에서 삭제하면 123,567이 되므로 1뒤의 ,와 4가 같이 없어져서 커서 위치가 2만큼 왼쪽으로 이동해야 한다. (최종 커서 위치 4가 되야함)
      //98,765에서 8,뒤(커서 위치 3)에서 삭제하면 9,765가 되므로 8만 없어져서 커서 위치가 1만큼 왼쪽으로 이동해야 한다. (최종 커서 위치 2가 되야함)
      selectionIndex = selectionIndex - (newText.replaceAll(",", "").length % 3 == 1 ? 2 : 1);
      newText = newValue.text.substring(0, newValue.selection.end - 1) + newValue.text.substring(newValue.selection.end);
      newText = thousandComma(newText, defaultVal: "0");
    }

    return newValue.copyWith(
        text: newText,
        selection: TextSelection.collapsed(
            offset: selectionIndex));
  }
}

 

 

참고 출처:

https://stackoverflow.com/questions/49577781/how-to-create-number-input-field-in-flutter

https://stackoverflow.com/questions/61866207/thousand-separator-without-decimal-separator-on-textformfield-flutter

반응형
:
Posted by 리샤씨
반응형

가끔가다, 파일 첨부 시 첨부된 파일의 용량을 보여줘야 할 때가 있다.

보통 파일 첨부 시 알 수 있는 것은 로우한 파일 크기인데,

필요한 건 해당 값을 1024로 나눈 값인 KB/MB/GB 등등이다.

 

처음엔 JS에서만 사용했었는데 최근 flutter에서도 필요하게 되어 두 가지 버전을 공유한다.

 

javascript 버전

function spaceFormat(spaceValue, decimalPoint){
	var fileSizeNmList = ["Byte", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
	var fileSizeNmIndex = 0;
	
	while (spaceValue > 1024 && fileSizeNmIndex < fileSizeNmList.length) {
		spaceValue /= 1024;
		fileSizeNmIndex++;
	}
	
	if (spaceValue.toFixed != undefined) {
		spaceValue = spaceValue.toFixed(decimalPoint);
	}
	return spaceValue + fileSizeNmList[fileSizeNmIndex];
}

 

flutter 버전

String spaceFormat(double spaceValue, int decimalPoint){
  List<String> fileSizeNmList = ["Byte", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
  int fileSizeNmIndex = 0;

  while (spaceValue > 1024 && fileSizeNmIndex < fileSizeNmList.length) {
    spaceValue /= 1024;
    fileSizeNmIndex++;
  }

  return spaceValue.toStringAsFixed(decimalPoint) + fileSizeNmList[fileSizeNmIndex];
}

 

반응형
:
Posted by 리샤씨
2022. 8. 3. 11:22

Flutter에서 Flick 화면 만들기 개발/Flutter2022. 8. 3. 11:22

반응형

일전에 Flick(한 방향으로 미는 것을 말한다고 한다. 참고 : https://thankee.tistory.com/117) 동작을 하는 화면을 요청 받은 적이 있었다.

그때는 한 방향으로 밀면 화면이 밀은 그대로 높이를 이동시키는 화면이었는데

최근 또 다른 프로젝트를 하는데 비슷한 동작을 하는 화면이 있어서

일전에 만든 소스를 활용해서 좀 더 범용성 있게 작성해보았다.

 

이번에 필요한 화면은 똑같은 동작으로 화면의 높이를 변화시키지만,

고정적인 세 가지 위치(최저/중간/최대)로만 이동이 가능한 기능과

최대 크기(화면 전체였다)로 키웠을 때 그림자가 사라지게 하는 기능이 필요해서 그 부분만 추가로 작업해주었다.

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class flickFrameCommon extends StatefulWidget {
  var mode; //soft, hard
  bool enabled;

  double height;
  double minHeight;
  double maxHeight;
  double middleHeight;

  EdgeInsets padding;
  Function changeHeight;//높이 값이 변화할 때 마다 flickFrameCommon를 호출한 곳에서 높이를 돌려 받을 수 있음
  Widget child;

  flickFrameCommon({
    Key key,
    this.mode = "soft",
    this.enabled = true,
    this.height = 52.0,
    this.minHeight,
    this.maxHeight,
    this.middleHeight,
    this.padding,
    this.changeHeight,
    @required this.child,
  }) : super(key: key);

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

class _flickFrameCommonState extends State<flickFrameCommon> {
  int shadowAlpha = 125;
  double prevHeight;
  double Sensitivity = 100; //감도

  @override
  void initState() {
    super.initState();
    prevHeight = widget.height;

    if (widget.minHeight == null) {
      if (30 < widget.height && widget.height < 118.7 ) {
        widget.minHeight = widget.height;
      } else {
        widget.minHeight = 118.7;
      }
    }

    if (widget.mode != "hard" && widget.mode != "soft") {
      throw ArgumentError("The mode is soft or hard");
    }

    if (widget.height < widget.minHeight) {
      throw ArgumentError("The height is greater than the minimum value");
    }

    if (widget.maxHeight == null) {
      if (333.7 < widget.height && widget.height <= Get.height) {
        widget.maxHeight = widget.height;
      } else {
        widget.maxHeight = 333.7;
      }
    }

    if (widget.height > widget.maxHeight) {
      throw ArgumentError("The height is less than the maximum value");
    }

    if (widget.padding == null) {
      widget.padding = EdgeInsets.only(left: 18.3, right: 18.3);
    }

    if (widget.mode == "hard") {
      if (widget.middleHeight == null) {
        throw ArgumentError("middleHeight is a must when in hard mode");
      }
      setHardHeight();
    }
  }

  @override
  Widget build(BuildContext context) {
    if (widget.height == widget.maxHeight) {
      shadowAlpha = 0;
    } else {
      shadowAlpha = 125;
    }

    return Positioned(
      bottom: 0,
      child: AnimatedContainer(
        width: Get.width,
        height: widget.height,
        duration: Duration(milliseconds: 500),
        padding: widget.padding,
        decoration: BoxDecoration(
          borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)),
          boxShadow: [BoxShadow(
              color: Color.fromARGB(shadowAlpha, 0, 0, 0),
              offset: Offset(0,3),
              blurRadius: 5,
              spreadRadius: 0,
          )],
          color: const Color(0xffffffff),
        ),
        child: Listener(
            behavior: HitTestBehavior.opaque,
            onPointerMove: (details){
              if (widget.enabled) {
                setState(() {
                //이동된 거리(details.delta.dy)는 위쪽으로 이동하면 -로 나오므로 현재 높이에 -를 해주면 +가 되어 높이가 커짐
                  if (widget.height - details.delta.dy < widget.minHeight) {//이동된 지점(현재 높이 - 이동된 거리)이 최저 높이보다 낮으면
                    widget.height = widget.minHeight;//높이를 최저 높이로 변경
                  } else
                  if (widget.height - details.delta.dy > widget.maxHeight) {//이동된 지점(현재 높이 - 이동된 거리)이 최대 높이보다 높으면
                    widget.height = widget.maxHeight;//높이를 최대 높이로 변경
                  } else {//최대와 최저 중간값이면 
                    widget.height = widget.height - details.delta.dy;//높이를 이동된 지점으로 변경
                  }
                });
              }
            },
            onPointerDown: (details) {//Pointer 이벤트가 시작할 때
              if (widget.enabled) {
                prevHeight = widget.height;//이전 높이를 저장
              }
            },
            onPointerUp: (details) {//Pointer 이벤트가 끝날 때
              if (widget.enabled) {
              	if (widget.mode == "hard") {
                  setState(() {
                    setHardHeight();//hard mode이면 최종 높이를 최저/중간/최대로 지정
                  });
                }

                if (widget.changeHeight != null) {
                  widget.changeHeight(widget.height);
                }
              }
            },
            child: Column(children: [
            Expanded(child: widget.child)
          ]),
        ),
      ),
    );
  }

  setHardHeight () {
    var minH_d = (widget.height - widget.minHeight).abs();//이벤트가 끝난 지점과 최저 높이의 거리 계산
    var maxH_d = (widget.height - widget.maxHeight).abs();//이벤트가 끝난 지점과 최대 높이의 거리 계산
    var middleH_d = (widget.height - widget.middleHeight).abs();//이벤트가 끝난 지점과 중간 높이의 거리 계산

    var direction = widget.height - prevHeight;

    if (prevHeight == widget.minHeight) {//이전 높이가 최저 높이였을 때
      if (direction > Sensitivity) {//감도보다 위쪽으로 이동한 거리가 높으면
        if (maxH_d < middleH_d) {//최대 높이의 거리가 중간 높이 거리보다 가까우면
          widget.height = widget.maxHeight;//현재 높이를 최대 높이로 변경
        } else {
          widget.height = widget.middleHeight;//현재 높이를 중간 높이로 변경
        }
      } else {
        widget.height = widget.minHeight;//감도가 낮으면 원래 높이로 이동
      }
    } else if (prevHeight == widget.maxHeight) {//이전 높이가 최대 높이였을 때
      if (direction < -1 * Sensitivity) {//감도보다 아래쪽으로 이동한 거리가 높으면
        if (minH_d < middleH_d) {//최저 높이의 거리가 중간 높이 거리보다 가까우면
          widget.height = widget.minHeight;//현재 높이를 최저 높이로 변경
        } else {
          widget.height = widget.middleHeight;//현재 높이를 중간 높이로 변경
        }
      } else {
        widget.height = widget.maxHeight;//감도가 낮으면 원래 높이로 이동
      }
    } else {
      if (direction > Sensitivity) {//감도보다 위쪽으로 이동한 거리가 높으면
        widget.height = widget.maxHeight;//현재 높이를 최대 높이로 변경
      } else if (direction < -1 * Sensitivity) {//감도보다 아래쪽으로 이동한 거리가 높으면
        widget.height = widget.minHeight;//현재 높이를 최저 높이로 변경
      } else {//감도가 낮으면 원래 높이로 이동
        widget.height = widget.middleHeight;//감도가 낮으면 원래 높이로 이동
      }
    }
  }
}

 

크게 soft(기본값) / hard mode가 있는데 soft mode가 이전에 사용했던 최저/최대 높이 내에서 자유롭게 움직일 수 있는 방식이고

hard mode가 이번에 새로 개발한 최저/중간/최고 높이로 이동하는 방식이다.

 

get은 3.26.0 버전을 사용하였고

Get.height와 Get.wight 현재 디바이스의 높이/넓이를 말한다.

다른 방식으로 높이와 넓이를 구할 수 있으면 사용하지 않아도 된다.

 

ex) 

import 'dart:async';

import 'package:blog/widget/flick_frame_common.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class Test extends StatefulWidget {
  @override
  _TestState createState() => _TestState();
}

class _TestState extends State<Test> {
  double realMaxHeight = Get.height;
  double height;
  double minHeight = 52.0;
  double middleHeight = 296.0;

  @override
  void initState() {
    super.initState();

    height = minHeight;

    Future.delayed(Duration.zero, () async {
      setState(() {
        realMaxHeight = Get.height
            - MediaQuery.of(context).padding.top
            - MediaQuery.of(context).padding.bottom;
      });
    });
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xffffffff),
      body: SafeArea(
        child: Container(
          width: Get.width,
          height: Get.height,
          child: Stack(children: [
            Container(
              width: Get.width,
              height: Get.height,
              alignment: Alignment.center,
              child: Text("Content Area"),
            ),
            flickFrameCommon(
              mode: "hard",
              height: height,
              padding: EdgeInsets.only(top: 20),
              minHeight: minHeight,
              middleHeight: middleHeight,
              maxHeight: realMaxHeight,
              changeHeight: (height) {
                setState(() {
                  this.height = height;
                });
              },
              child: Column(
                children: [
                  Text("Flick Frame Area")
                ],
              ),
            )
          ]),
        )
      ),
    );
  }
}

 

왼쪽 부터 순서대로 Filck Frame이 최저/중간/최고 높이일 때 화면

반응형
:
Posted by 리샤씨
반응형

업데이트 체크를 new_version 패키지로 잘 하고 있었는데 어느 순간 아래와 같이

Bad state: No element Exception이 발생하면서 정상 작동되지 않았다.

E/flutter ( 5782): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: Bad state: No element
E/flutter ( 5782): #0      ListMixin.firstWhere (dart:collection/list.dart:167:5)
E/flutter ( 5782): #1      NewVersion._getAndroidStoreVersion (package:new_version/new_version.dart:152:51)

 

확인 결과,

Future<VersionStatus?> _getAndroidStoreVersion(
      PackageInfo packageInfo) async {
    final id = androidId ?? packageInfo.packageName;
    final uri =
        Uri.https("play.google.com", "/store/apps/details", {"id": "$id"});
    final response = await http.get(uri);
    if (response.statusCode != 200) {
      debugPrint('Can\'t find an app in the Play Store with the id: $id');
      return null;
    }
    final document = parse(response.body);

    final additionalInfoElements = document.getElementsByClassName('hAyfc');
    final versionElement = additionalInfoElements.firstWhere(
      (elm) => elm.querySelector('.BgcNfc')!.text == 'Current Version',
    );
    final storeVersion = versionElement.querySelector('.htlgb')!.text;

    final sectionElements = document.getElementsByClassName('W4P4ne');
    final releaseNotesElement = sectionElements.firstWhereOrNull(
      (elm) => elm.querySelector('.wSaTQd')!.text == 'What\'s New',
    );
    final releaseNotes = releaseNotesElement
        ?.querySelector('.PHBdkd')
        ?.querySelector('.DWPxHb')
        ?.text;

    return VersionStatus._(
      localVersion: packageInfo.version,
      storeVersion: storeVersion,
      appStoreLink: uri.toString(),
      releaseNotes: releaseNotes,
    );
  }

위의 소스에서 보이듯이 new_version에서 Android는 https://play.google.com/store/apps/details 페이지로 접속해서

hAyfc 클래스를 가져와서 현재 스토어에 올라온 버전이 무엇인지 확인하는 과정이 있는데 

플레이 스토어가 리뉴얼 하면서 그냥 https://play.google.com/store/apps/details?id=com.nhn.android.search로 접속하면 

hAyfc 클래스가 없기 때문에 정상 작동은 커녕 오류를 발생하는 것으로 보였다.

 

이전과 같은 페이지를 보려면 다음과 같이 파라메터에 gl=us를 추가로 보내주면

(https://play.google.com/store/apps/details?id=com.nhn.android.search&gl=us)

hAyfc 클래스가 존재해서, 위 소스의 5번째 줄에 아래 처럼 코드를 수정하면 정상 동작하는 것을 확인하였다.

Uri.https("play.google.com", "/store/apps/details", {"id": "$id", "gl": "US"});

 

해당 소스의 위치는 나는 mac 환경이고 new_version 버전은 0.2.3을 사용하였기 때문에

/Users/${userId}/.pub-cache/hosted/pub.dartlang.org/new_version-0.2.3/lib/new_version.dart

여기에 저장되어 있었다.

 

 

 

 

 

반응형
:
Posted by 리샤씨
2021. 11. 11. 11:58

webview_flutter로 alert/confirm 띄우기 (iOS) 개발/Flutter2021. 11. 11. 11:58

반응형

이전에도 비슷한 주제로  글을 작성한 적이 있었는데 이번엔 해결법이 약간 다르다.

(이전 글 : https://risha-lee.tistory.com/40)

 

이번에는 상황이 Confirm 종류도 많았고,

그냥 웹뷰를 보여주는 앱이었으며
웹은 이미 작성된지 오래된 상태였고

Confirm 이후 분기 처리도 꽤 복잡한 편이어서 꼭 웹의 Confirm 기능을 써야만 했다.

 

일단 Android는 webview_flutter를 2.0.13으로 버전 업하니 alert가 기본으로 작동하게 되었는데 

iOS에서는 여전히 alert/confirm이 안뜨는 문제가 있었다.

찾아보니 역시나 webview_flutter 자체 지원은 안되고 파일 업로드 때 처럼 직접 소스 코드를 수정하기로 하였다.

 

1. 먼저 webview_flutter가 저장되는 위치를 찾는다.

나는 mac 환경이고 webview_flutter 버전은 2.0.13을 사용하였기 때문에

/Users/${userId}/.pub-cache/hosted/pub.dartlang.org/webview_flutter-2.0.13

여기에 저장되어 있었다.

 

2. webview_flutter-2.0.13/ios/Classes/FlutterWebView.m 의 맨 하단 @end 이전에

다음을 추가한다.

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
  [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
  completionHandler();
  }])];

    UIViewController *_viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
  [_viewController presentViewController:alertController animated:YES completion:nil];
}

- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
  //    DLOG(@"msg = %@ frmae = %@",message,frame);
  UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
  [alertController addAction:([UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
    completionHandler(NO);
  }])];
  [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    completionHandler(YES);
  }])];

    UIViewController *_viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
  [_viewController presentViewController:alertController animated:YES completion:nil];
}

 

 

runJavaScriptTextInputPanelWithPrompt라고 prompt 처리하는 것으로 보이는 것도 있긴한데

내가 작업하는 웹뷰에서 사용하지 않아서 확인하진 못하였으나 비슷한 방식으로 변환하면 다음과 같다.

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
  UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
  [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
    textField.text = defaultText;
  }];
  [alertController addAction:([UIAlertAction actionWithTitle:@"" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    completionHandler(alertController.textFields[0].text?:@"");
  }])];
  UIViewController *_viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
  [_viewController presentViewController:alertController animated:YES completion:nil];
}

 

 

 

참고 출처 : https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/695084/

https://stackoverflow.com/questions/6131205/how-to-find-topmost-view-controller-on-ios

반응형
:
Posted by 리샤씨
2021. 11. 9. 12:52

webview_flutter로 file upload하기 (Android) 개발/Flutter2021. 11. 9. 12:52

반응형

최근 flutter로 웹뷰에서 파일 업로드해야 하는 일이 생겼는데 

Android webview_flutter(https://pub.dev/packages/webview_flutter)에서

그냥 input file 은 작동하지 않는 것을 확인하였다.

 

검색해보니 flutter_webview_plugin(https://pub.dev/packages/flutter_webview_plugin)를

이용하면 별도 작업없이 바로 되는 것 같아 보였다.

하지만 flutter_webview_plugin로는 이미 개발해둔 javasrcipt와 flutter의 통신

페이지 네비게이션 등 처리가 될 것 같지 않아보여서 다른 방법을 더 찾아보았다.

 

결국 직접적으로 webview_flutter의 소스를 건드는 방법을 알게되어 그 소스를 공유하도록 하겠다.

 

1. 먼저 webview_flutter가 저장되는 위치를 찾는다.

나는 mac 환경이고 webview_flutter 버전은 2.0.13을 사용하였기 때문에

/Users/${userId}/.pub-cache/hosted/pub.dartlang.org/webview_flutter-2.0.13

여기에 저장되어 있었다.

 

2. webview_flutter-2.0.13/android/src/main/AndroidManifest.xml를 아래 소스로 수정한다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="io.flutter.plugins.webviewflutter">
<application>
 <provider
     android:name="io.flutter.plugins.webviewflutter.GenericFileProvider"
     android:authorities="${applicationId}.generic.provider"
     android:exported="false"
     android:grantUriPermissions="true"
     >
     <meta-data
         android:name="android.support.FILE_PROVIDER_PATHS"
         android:resource="@xml/provider_paths"
         />
 </provider>
 <activity android:name="io.flutter.plugins.webviewflutter.FileChooserActivity">
 </activity>
 <activity android:name="io.flutter.plugins.webviewflutter.RequestCameraPermissionActivity">
 </activity>

 </application>
 <queries>
     <package android:name="com.google.android.apps.photos" />
     <package android:name="com.google.android.apps.docs" />
     <package android:name="com.google.android.documentsui" />
 </queries>
</manifest>

 

3. webview_flutter-2.0.13/android/src/main/res/values/strings.xml를 추가한다.

경로에 폴더가 없다면 폴더도 추가한다.

<resources>
    <string name="webview_file_chooser_title">Choose a file</string>
    <string name="webview_image_chooser_title">Choose an image</string>
    <string name="webview_video_chooser_title">Choose a video</string>
</resources>

 

4. webview_flutter-2.0.13/android/src/main/res/xml/provider_paths.xml를 추가한다.

경로에 폴더가 없다면 폴더도 추가한다.

 <paths xmlns:android="http://schemas.android.com/apk/res/android">
     <external-files-path name="safetyapp_images" path="images" />
 </paths>

 

5. webview_flutter-2.0.13/android/src/main/java/io/flutter/plugins/webviewflutter/Constants.java를 추가합니다.

package io.flutter.plugins.webviewflutter;

public class Constants {
    static final String ACTION_REQUEST_CAMERA_PERMISSION_FINISHED =
            "action_request_camera_permission_denied";
    static final String ACTION_FILE_CHOOSER_FINISHED = "action_file_chooser_completed";

    static final String EXTRA_TITLE = "extra_title";
    static final String EXTRA_ACCEPT_TYPES = "extra_types";
    static final String EXTRA_SHOW_VIDEO_OPTION = "extra_show_video_option";
    static final String EXTRA_SHOW_IMAGE_OPTION = "extra_show_image_option";
    static final String EXTRA_FILE_URIS = "extra_file_uris";
    static final String EXTRA_ALLOW_MULTIPLE_FILES = "extra_allow_multiple_files";


    static final String WEBVIEW_STORAGE_DIRECTORY = "images";
}

 

6. webview_flutter-2.0.13/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserActivity.java를 추가한다.

package io.flutter.plugins.webviewflutter;

import static io.flutter.plugins.webviewflutter.Constants.ACTION_FILE_CHOOSER_FINISHED;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_ACCEPT_TYPES;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_ALLOW_MULTIPLE_FILES;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_FILE_URIS;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_SHOW_IMAGE_OPTION;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_SHOW_VIDEO_OPTION;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_TITLE;
import static io.flutter.plugins.webviewflutter.Constants.WEBVIEW_STORAGE_DIRECTORY;

import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

public class FileChooserActivity extends Activity {

private static final int FILE_CHOOSER_REQUEST_CODE = 12322;
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");

// List of Uris that point to files where there MIGHT be the output of the capture. At most one of these can be valid
private final ArrayList<Uri> potentialCaptureOutputUris = new ArrayList<>();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    showFileChooser(
            getIntent().getBooleanExtra(EXTRA_SHOW_IMAGE_OPTION, false),
            getIntent().getBooleanExtra(EXTRA_SHOW_VIDEO_OPTION, false));
}

private void showFileChooser(boolean showImageIntent, boolean showVideoIntent) {
    Intent getContentIntent = createGetContentIntent();
    Intent captureImageIntent =
            showImageIntent ? createCaptureIntent(MediaStore.ACTION_IMAGE_CAPTURE, "jpg") : null;
    Intent captureVideoIntent =
            showVideoIntent ? createCaptureIntent(MediaStore.ACTION_VIDEO_CAPTURE, "mp4") : null;

    if (getContentIntent == null && captureImageIntent == null && captureVideoIntent == null) {
        // cannot open anything: cancel file chooser
        sendBroadcast(new Intent(ACTION_FILE_CHOOSER_FINISHED));
        finish();
    } else {
        ArrayList<Intent> intentList = new ArrayList<>();

        if (getContentIntent != null) {
            intentList.add(getContentIntent);
        }

        if (captureImageIntent != null) {
            intentList.add(captureImageIntent);
        }
        if (captureVideoIntent != null) {
            intentList.add(captureVideoIntent);
        }

        Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
        chooserIntent.putExtra(Intent.EXTRA_TITLE, getIntent().getStringExtra(EXTRA_TITLE));

        chooserIntent.putExtra(Intent.EXTRA_INTENT, intentList.get(0));
        intentList.remove(0);
        if (intentList.size() > 0) {
            chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentList.toArray(new Intent[0]));
        }

        startActivityForResult(chooserIntent, FILE_CHOOSER_REQUEST_CODE);
    }
}

private Intent createGetContentIntent() {
    Intent filesIntent = new Intent(Intent.ACTION_GET_CONTENT);

    if (getIntent().getBooleanExtra(EXTRA_ALLOW_MULTIPLE_FILES, false)) {
        filesIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
    }

    String[] acceptTypes = getIntent().getStringArrayExtra(EXTRA_ACCEPT_TYPES);

    if (acceptTypes.length == 0 || (acceptTypes.length == 1 && acceptTypes[0].length() == 0)) {
        // empty array or only 1 empty string? -> accept all types
        filesIntent.setType("*/*");
    } else if (acceptTypes.length == 1) {
        filesIntent.setType(acceptTypes[0]);
    } else {
        // acceptTypes.length > 1
        filesIntent.setType("*/*");
        filesIntent.putExtra(Intent.EXTRA_MIME_TYPES, acceptTypes);
    }

    return (filesIntent.resolveActivity(getPackageManager()) != null) ? filesIntent : null;
}

private Intent createCaptureIntent(String type, String fileFormat) {
    Intent captureIntent = new Intent(type);
    if (captureIntent.resolveActivity(getPackageManager()) == null) {
        return null;
    }

    // Create the File where the output should go
    Uri captureOutputUri = getTempUri(fileFormat);
    potentialCaptureOutputUris.add(captureOutputUri);

    captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureOutputUri);

    return captureIntent;
}
private File getStorageDirectory() {
    File imageDirectory = new File(this.getExternalFilesDir(null), WEBVIEW_STORAGE_DIRECTORY);
    if (!imageDirectory.isDirectory()) {
        imageDirectory.mkdir();
    }
    return imageDirectory;
}


private Uri getTempUri(String format) {
    String fileName = "CAPTURE-" + simpleDateFormat.format(new Date()) + "." + format;
    File file = new File(getStorageDirectory(), fileName);
    return FileProvider.getUriForFile(
            this, getApplicationContext().getPackageName() + ".generic.provider", file);
}

private String getFileNameFromUri(Uri uri) {
    Cursor returnCursor = getContentResolver().query(uri, null, null, null, null);
    assert returnCursor != null;
    int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
    returnCursor.moveToFirst();
    String name = returnCursor.getString(nameIndex);
    returnCursor.close();
    return name;
}

private Uri copyToLocalUri(Uri uri) {
    File destination = new File(getStorageDirectory(), getFileNameFromUri(uri));

    try (InputStream in = getContentResolver().openInputStream(uri);
         OutputStream out = new FileOutputStream(destination)) {
        byte[] buffer = new byte[1024];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        return FileProvider.getUriForFile(
                this, getApplicationContext().getPackageName() + ".generic.provider", destination);
    } catch (IOException e) {
        Log.e("WEBVIEW", "Unable to copy selected image", e);
        e.printStackTrace();
        return null;
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
        Intent fileChooserFinishedIntent = new Intent(ACTION_FILE_CHOOSER_FINISHED);
        if (resultCode == Activity.RESULT_OK) {
            if (data != null && (data.getDataString() != null || data.getClipData() != null)) {
                if (data.getDataString() != null) {
                    // single result from file browser OR video from camera
                    Uri localUri = copyToLocalUri(data.getData());
                    if (localUri != null) {
                        fileChooserFinishedIntent.putExtra(
                                EXTRA_FILE_URIS, new String[] {localUri.toString()});
                    }
                } else if (data.getClipData() != null) {
                    // multiple results from file browser
                    int uriCount = data.getClipData().getItemCount();
                    String[] uriStrings = new String[uriCount];

                    for (int i = 0; i < uriCount; i++) {
                        Uri localUri = copyToLocalUri(data.getClipData().getItemAt(i).getUri());
                        if (localUri != null) {
                            uriStrings[i] = localUri.toString();
                        }
                    }
                    fileChooserFinishedIntent.putExtra(EXTRA_FILE_URIS, uriStrings);
                }
            } else {
                // image result from camera (videos from the camera are handled above, but this if-branch could handle them too if this varies from device to device)
                for (Uri captureOutputUri : potentialCaptureOutputUris) {
                    try {
                        // just opening an input stream (and closing immediately) to test if the Uri points to a valid file
                        // if it's not a real file, the below catch-clause gets executed and we continue with the next Uri in the loop.
                        getContentResolver().openInputStream(captureOutputUri).close();
                        fileChooserFinishedIntent.putExtra(
                                EXTRA_FILE_URIS, new String[] {captureOutputUri.toString()});
                        // leave the loop, as only one of the potentialCaptureOutputUris is valid and we just found it
                        break;
                    } catch (IOException ignored) {
                    }
                }
            }
        }
        sendBroadcast(fileChooserFinishedIntent);
        finish();
    } else {
        super.onActivityResult(requestCode, resultCode, data);
    }
   }
 }

 

7. webview_flutter-2.0.13/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserLauncher.java를 추가한다.

package io.flutter.plugins.webviewflutter;

import static io.flutter.plugins.webviewflutter.Constants.ACTION_FILE_CHOOSER_FINISHED;
import static io.flutter.plugins.webviewflutter.Constants.ACTION_REQUEST_CAMERA_PERMISSION_FINISHED;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_ACCEPT_TYPES;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_ALLOW_MULTIPLE_FILES;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_FILE_URIS;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_SHOW_IMAGE_OPTION;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_SHOW_VIDEO_OPTION;
import static io.flutter.plugins.webviewflutter.Constants.EXTRA_TITLE;

import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.webkit.ValueCallback;
import androidx.core.content.ContextCompat;

public class FileChooserLauncher extends BroadcastReceiver {

private Context context;
private String title;
private boolean allowMultipleFiles;
private boolean videoAcceptable;
private boolean imageAcceptable;
private ValueCallback<Uri[]> filePathCallback;
private String[] acceptTypes;

public FileChooserLauncher(
        Context context,
        boolean allowMultipleFiles,
        ValueCallback<Uri[]> filePathCallback,
        String[] acceptTypes) {
    this.context = context;
    this.allowMultipleFiles = allowMultipleFiles;
    this.filePathCallback = filePathCallback;
    this.acceptTypes = acceptTypes;

    if (acceptTypes.length == 0 || (acceptTypes.length == 1 && acceptTypes[0].length() == 0)) {
        // acceptTypes empty -> accept anything
        imageAcceptable = true;
        videoAcceptable = true;
    } else {
        for (String acceptType : acceptTypes) {
            if (acceptType.startsWith("image/")) {
                imageAcceptable = true;
            } else if (acceptType.startsWith("video/")) {
                videoAcceptable = true;
            }
        }
    }

    if (imageAcceptable && !videoAcceptable) {
        title = context.getResources().getString(R.string.webview_image_chooser_title);
    } else if (videoAcceptable && !imageAcceptable) {
        title = context.getResources().getString(R.string.webview_video_chooser_title);
    } else {
        title = context.getResources().getString(R.string.webview_file_chooser_title);
    }
}

private boolean canCameraProduceAcceptableType() {
    return imageAcceptable || videoAcceptable;
}

private boolean hasCameraPermission() {
    return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
            == PackageManager.PERMISSION_GRANTED;
}

public void start() {
    if (!canCameraProduceAcceptableType() || hasCameraPermission()) {
        showFileChooser();
    } else {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ACTION_REQUEST_CAMERA_PERMISSION_FINISHED);
        context.registerReceiver(this, intentFilter);

        Intent intent = new Intent(context, RequestCameraPermissionActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }
}

private void showFileChooser() {
    IntentFilter intentFilter = new IntentFilter(ACTION_FILE_CHOOSER_FINISHED);
    context.registerReceiver(this, intentFilter);

    Intent intent = new Intent(context, FileChooserActivity.class);
    intent.putExtra(EXTRA_TITLE, title);
    intent.putExtra(EXTRA_ACCEPT_TYPES, acceptTypes);
    intent.putExtra(EXTRA_SHOW_IMAGE_OPTION, imageAcceptable && hasCameraPermission());
    intent.putExtra(EXTRA_SHOW_VIDEO_OPTION, videoAcceptable && hasCameraPermission());
    intent.putExtra(EXTRA_ALLOW_MULTIPLE_FILES, allowMultipleFiles);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(intent);
}

@Override
public void onReceive(Context context, Intent intent) {
    if (intent.getAction().equals(ACTION_REQUEST_CAMERA_PERMISSION_FINISHED)) {
        context.unregisterReceiver(this);
        showFileChooser();
    } else if (intent.getAction().equals(ACTION_FILE_CHOOSER_FINISHED)) {
        String[] uriStrings = intent.getStringArrayExtra(EXTRA_FILE_URIS);
        Uri[] result = null;

        if (uriStrings != null) {
            int uriStringCount = uriStrings.length;
            result = new Uri[uriStringCount];

            for (int i = 0; i < uriStringCount; i++) {
                result[i] = Uri.parse(uriStrings[i]);
            }
        }

        filePathCallback.onReceiveValue(result);
        context.unregisterReceiver(this);
        filePathCallback = null;
    }
  }
 }

 

8. webview_flutter-2.0.13/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java

class FlutterWebChromeClient안에 메소드를 추가한다.

import android.net.Uri;
import android.webkit.ValueCallback;


    @Override
    public boolean onShowFileChooser(
            WebView webView,
            ValueCallback<Uri[]> filePathCallback,
            FileChooserParams fileChooserParams) {
      // info as of 2021-03-08:
      // don't use fileChooserParams.getTitle() as it is (always? on                Mi 9T Pro Android 10 at least) null
      // don't use fileChooserParams.isCaptureEnabled() as it is (always? on Mi 9T Pro Android 10 at least) false, even when the file upload allows images or any file
      final Context context = webView.getContext();
      final boolean allowMultipleFiles = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
              && fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;
      final String[] acceptTypes = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
              ? fileChooserParams.getAcceptTypes() : new String[0];
      new FileChooserLauncher(context, allowMultipleFiles, filePathCallback, acceptTypes)
              .start();
      return true;
    }

 

9. webview_flutter-2.0.13/android/src/main/java/io/flutter/plugins/webviewflutter/RequestCameraPermissionActivity.java를 추가합니다.

package io.flutter.plugins.webviewflutter;

import android.Manifest; 
import android.app.Activity; 
import android.content.Intent; 
import android.os.Bundle; 
import androidx.annotation.NonNull; 
import androidx.annotation.Nullable; 
import androidx.core.app.ActivityCompat;
import static io.flutter.plugins.webviewflutter.Constants.ACTION_REQUEST_CAMERA_PERMISSION_FINISHED;

public class RequestCameraPermissionActivity extends Activity {

    private static final int CAMERA_PERMISSION_REQUEST_CODE = 12321;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActivityCompat.requestPermissions(
                this, new String[] {Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
    }

    @Override
    public void onRequestPermissionsResult(
            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
            sendBroadcast(new Intent(ACTION_REQUEST_CAMERA_PERMISSION_FINISHED));
            finish();
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

 

10. webview_flutter-2.0.13/android/src/main/java/io/flutter/plugins/webviewflutter/GenericFileProvider.java를 추가한다.

package io.flutter.plugins.webviewflutter;

import androidx.core.content.FileProvider;

public class GenericFileProvider extends FileProvider {

    public static final String PROVIDER_NAME = "io.flutter.plugins.webviewflutter.GenericFileProvider";

}

 

11. input file 클릭시 카메라로 직접 사진을 찍어 파일을 올리게 하려면 원래 flutter project에서 android/app/src/main/AndroidManifest.xml 에 <uses-permission android:name="android.permission.CAMERA"/> 권한을 추가한다. (선택)

 

카메라 권한 확인 팝업 / 파일 선택 팝업

 

 

 

출처 :

https://stackoverflow.com/questions/60289932/is-there-a-way-of-making-a-flutter-webview-use-android-camera-for-file-upload-h
https://github.com/pubnative/easy-files-android/blob/master/app/src/main/java/net/easynaps/easyfiles/utils/GenericFileProvider.java

https://m.blog.naver.com/websearch/221738662288

 

 

 

 

반응형
:
Posted by 리샤씨
반응형

며칠 전, Flutter에서 webview_flutter(https://pub.dev/packages/webview_flutter)로 띄운 소스에서

alert가 보이지 않으니 해결해달라는 요청을 받아 처리하게 되었습니다.

찾아보니 webview_flutter는 alert&confirm을 지원하지 않는다고 합니다.

flutter와 html에서 약속을 해서 새로운 function을 만들고 호출하는 방법도 있지만,

(참조: https://cording-cossk3.tistory.com/103)

저는 html 개발하시는 분과 직접 소통하는 것도 아니고, 최대한 네이티브 소스는 건드리지 않기 위하여

그냥 기존에 있는 alert를 flutter의 toast로 작업하는 방식으로 처리하였습니다.

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:toast/toast.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebviewPage extends StatelessWidget {
  WebViewController _webViewController;

  @override
  void initState() {
    if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
  }

  @override
  Widget build(BuildContext context) {
    return WebView(
      initialUrl: '${yourUrl}',
      javascriptMode: JavascriptMode.unrestricted,
      javascriptChannels: <JavascriptChannel>{
        _alertJavascriptChannel(context),
      },
      onWebViewCreated: (WebViewController webViewController) {
        _webViewController = webViewController;
      },
      onPageFinished: (url) async {
        try {
          var javascript = '''
                             window.alert = function (e){
                               Alert.postMessage(e);
                             }
                           ''';
          await _webViewController?.evaluateJavascript(javascript);
        } catch (_) {}
      },
    );
  }

  JavascriptChannel _alertJavascriptChannel(BuildContext context) {
    return JavascriptChannel(
        name: 'Alert',
        onMessageReceived: (JavascriptMessage message) {
          Toast.show(message.message, context,
              duration: Toast.LENGTH_LONG,
              gravity: Toast.CENTER,
              backgroundColor: Colors.black38,
              backgroundRadius: 5);
        });
  }
}

이 방법으로 하면 confirm도 처리되긴 합니다.

그런데 confirm이 하나일땐 크게 문제 없겠지만 (위와 같이 confirm을 정의해서 flutter 내에서 dialog 호출해서 처리)

여러 개일때는 confirm마다 기능이 다르다면 결국 html 개발자와 같이 상의해서 개발해야 할 것입니다.

confirm도 처리해야 할 때는 android는 webview_flutter를 2.0.13 버전 이상으로 업 하면 해결됩니다.

ios는 다음 글을 참고하여 처리하여 주세요. (다음 글 : https://risha-lee.tistory.com/42)

 

 

참고 출처 : https://github.com/flutter/flutter/issues/30358

 

 

 

 

 

 

 

반응형
:
Posted by 리샤씨


반응형
반응형