2023年8月2日 星期三

[Android] Jetpack Compose Text With Underline

 參考這篇文章後的補充。

取得TextLayoutResult的方法有兩種。

第一種:

    val textLayoutResultState = remember { mutableStateOf<textlayoutresult>(null) }
    Text(
        text = text,
        style = style,
        color = color,
        textAlign = textAlign,
        onTextLayout = {
            textLayoutResultState.value = it
        }
    )

第二種(style就包含fontSize和color):

    val textMeasurer = rememberTextMeasurer()
    val textResult = textMeasurer.measure(
           text = text,
           style = style
    )

取得TextLayoutResult後就可以讀取每個字空間大小

    text.indices.forEach { index ->
        val rect = textLayoutResultState.value!!.getBoundingBox(index)
        rectList.add(rect)
    }

如果要找特定字串的話可使用這方法

    val findText = "find some text"
    val startIndex = text.indexOf(findText)
    val endIndex = startIndex.plus(findText.length) - 1
    (startIndex..endIndex).forEach { index ->
        val rect = textLayoutResultState.value!!.getBoundingBox(index)
        rectList.add(rect)
    }

其中wave path的方法補充(對Path的擴展):

    const val TWO_PI = 2 * Math.PI.toFloat()
    private fun Path.buildWaveLinePath(bound: Rect, waveLength: Float, animProgress: Float): Path {
        asAndroidPath().rewind()
        var pointX = bound.left
        while (pointX < bound.right) {
            val offsetY =
                bound.bottom + sin(((pointX - bound.left) / waveLength)
                	* TWO_PI + (TWO_PI * animProgress)) * 10
            if (pointX == bound.left) {
                moveTo(bound.left, offsetY)
            }
            lineTo(pointX, offsetY)
            pointX += 1F
        }
        return this
}

使用wave path的方法:

    val infiniteTransition = rememberInfiniteTransition(label = "")
    val animProgress = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(500, easing = LinearEasing)
        ), 
        label = ""
    )
    val start = rectList.first()!!.topLeft
    val end = rectList.last()!!.bottomRight
    val rect = Rect(start, end)
    val path = Path().buildWaveLinePath(rect, 50f, animProgress.value)
    val pathStyle = Stroke(
        width = 5f,
        pathEffect = PathEffect.cornerPathEffect(radius = 9.dp.toPx())
    )
    
    drawPath(
        color = Color.Yellow,
        path = path,
        style = pathStyle
    )    

2023年8月1日 星期二

[Android] Create Outlined Text Using Jetpack Compose

在這篇文章中,介紹了兩種方式製作OutlinedText。

第一種:使用drawStyle(限制andriodx.core:core-ktx版本需要1.4.0-alpha01以上才有)

Text(
    text = "Sample",
    style = TextStyle.Default.copy(
        fontSize = 64.sp,
        drawStyle = Stroke(
            miter = 10f,
            width = 5f,
            join = StrokeJoin.Round
        )
    )
)

缺點:沒有繪製文字,只有文字外框


第二種:使用Canvas

// Creating a outline text
@Composable
fun OutLineText() {

    // Create a Paint that has black stroke
    val textPaintStroke = Paint().asFrameworkPaint().apply {
        isAntiAlias = true
        style = android.graphics.Paint.Style.STROKE
        textSize = 100f
        color = android.graphics.Color.CYAN
        strokeWidth = 12f
        strokeMiter = 10f
        strokeJoin = android.graphics.Paint.Join.ROUND
    }

    // Create a Paint that has white fill
    val textPaint = Paint().asFrameworkPaint().apply {
        isAntiAlias = true
        style = android.graphics.Paint.Style.FILL
        textSize = 100f
        color = android.graphics.Color.WHITE
    }

    // Create a canvas, draw the black stroke and
    // override it with the white fill
    Canvas(
        modifier = Modifier.fillMaxSize(),
        onDraw = {
            drawIntoCanvas {
                it.nativeCanvas.drawText(
                        "Hello World",
                        200f,
                        200.dp.toPx(),
                        textPaintStroke
                    )

                    it.nativeCanvas.drawText(
                        "Hello World",
                        200f,
                        200.dp.toPx(),
                        textPaint
                    )
                }
            }
        )
}

缺點: 這方式能繪出文字及顏色,

            但因為使用了Canvas,所以是以畫面有效區域內的座標為位置,

            無法使用Arrangement.Center,快速置中,

            必須自行算出中點扣除文字長度一半後的座標點位。

            (無法將文字當物件使用)


將上兩種方法重新整合後,出現第三種方法

@Composable
fun OutLineText(
    text: String,
    modifier: Modifier = Modifier,
    fontSize: TextUnit,
    color: Color = Color.White,
    textAlign: TextAlign = TextAlign.Start,
    outlinedBrush: Brush = Brush.horizontalGradient(
        listOf(
            Color.Blue,
            Color.Magenta,
            Color.Red,
            Color.Yellow
        )
    ),
) {
    val textLayoutResultState = remember { mutableStateOf(null) }
    Text(
        text = text,
        fontSize = fontSize,
        color = color,
        textAlign = textAlign,
        onTextLayout = { textLayoutResultState.value = it },
        modifier = modifier
            .drawWithCache {
                onDrawBehind {
                    drawIntoCanvas {
                        drawText(
                            textLayoutResultState.value!!,
                            outlinedBrush,
                            drawStyle = Stroke(
                                miter = 20f,
                                width = 15f,
                                join = StrokeJoin.Round
                            ),
                        )
                    }
                }
            }
            .width(IntrinsicSize.Max)
    )
}

draw方法有兩種,drawWithContent和drawBehind,

drawWithContent多了一個drawContent()的方法,

讓你決定文字本體的繪製順序,

則drawBehind會繪製有在文字本體之下,背景之上,


drawWithCache提供兩個方法,

onDrawWithContent和onDrawBehind,

使用方式相同,只是結果會存在Cache中,

UI重新繪製時先檢查Cache是否有相同的結果,

如果有,就不重新計算,進而增加效能。,


想了解draw方法可以參考這篇

更進階的用法可參考這篇

2022年10月20日 星期四

[Android] 使用ViewBinding建立BaseFragment

先在build.gradle內開啟viewBinding。

    buildFeatures{
        viewBinding true
        dataBinding true
    }

開啟後在res->layout內創建一個layout(my_fragment.xml),
Android會自動生成一個類名稱與layout名字相同的viewBinding(MyFragmentBinding),
可以使用下面的程式碼產生view。

public class MyFragment extends Fragment {
    MyFragmentBinding viewBinding;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        viewBinding = MyFragmentBinding.inflate(inflater, container, false);
        return viewBinding.getRoot();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        viewBinding = null;
    }
}

但當Fragment很多時,每創建一個就必須寫重復的程式碼,所以可以把重復的程式碼提取到一個父類別,每個Fragment之間的差別只有在 viewBinding的類不同,但都是繼承ViewBinding,所以可以使用泛型的方式,告訴父類別在子類別中的ViewBinding是什麼類別名稱,將父類別設成abstract,再設定abstract方法取得子類別的viewBinding物件。

public abstract class BaseFragment<V extends Viewbinding> extends Fragment {
    public V viewBinding;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        viewBinding = getBindingInflater().invoke(inflater, container, savedInstanceState);
        return viewBinding.getRoot();
    }
    
    //取得子類的viewBinding物件
    public abstract Function3<LayoutInflater, ViewGroup, Bundle, V> getBindingInflater();

    @Override
    public void onDestroy() {
        super.onDestroy();
        viewBinding = null;
    }
}

在MyFragment中只要告訴BaseFragment子類別的viewBinding是什麼類別(MyFragmentBinding),再將viewBinding透過實作方法回傳給父類別即可。

public class MyFragment extends BaseFragment<MyFragmentBinding> {
    MyFragmentBinding viewBinding;
    
    @Override
    public Function3<LayoutInflater, ViewGroup, Bundle, MyFragmentBinding> getBindingInflater() {
        return (o1,o2,o3)->MyFragmentBinding.inflate(o1,o2,false);
    }
}

Funtion3也可與以下Get方法相同,最大的差別是Funtion3可用lambda表達式。

public abstract V getBindingInflater(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
public abstract Function3<LayoutInflater, ViewGroup, Bundle, V> getBindingInflater();

2022年5月15日 星期日

[Window Form] GMap的客製marker

  
    internal class CustomMarker : GMapMarker
    {
        //GMapMarker沒有Font屬性,所以自己新增
        private Font Font { get; set; } = new Font(FontFamily.GenericSansSerif, 8f, FontStyle.Bold);

        public CustomMarker(PointLatLng pos) : base(pos)
        {
            Size = new Size(20, 20);
            //Marker置中,點擊位置為中心
            Offset = new Point(-Size.Width / 2, -Size.Height / 2);
            Tag = "9";
        }

        public override void OnRender(Graphics g)
        {
            base.OnRender(g);
            var x = LocalArea.X;
            var y = LocalArea.Y;
            var w = LocalArea.Width;
            var h = LocalArea.Height;
            g.FillEllipse(Brushes.White, LocalArea);
            g.DrawEllipse(Pens.Black, LocalArea);
            //這使用Tag屬性記錄文字內容,也可使用其他屬性
            var tag = Tag?.ToString();
            //計笡文字大小
            var textSize = TextRenderer.MeasureText(tag, Font);
            g.DrawString(tag, Font, Brushes.Black, new PointF(x + (w - textSize.Width) / 2f + 1.8f, y + (h - textSize.Height) / 2f));
        }
    }

使用方法:

    Form1.cs
    //設定一組經緯座標,也可是滑鼠點擊的位置轉換而來(FromLocalToLatLng)
    //var position =  gMapControl1.FromLocalToLatLng(int x, int y);
    var position = new PointLatLng(24.064, 120.704);
    var marker = new CustomMarker(position);
    var overlay1 = new GMapOverlay("mark");
    overlay1.Markers.Add(marker);
    gMapControl1.Overlays.Add(overlay1);

2022年5月5日 星期四

[Window Form] GMap常用指令

初始化常用參數:

    //設定地圖提供者
    gMapControl1.MapProvider = GMap.NET.MapProviders.GMapProviders.OpenStreetMap;//較多可選擇
    gMapControl1.MapProvider = GMap.NET.MapProviders.GoogleChinaHybridMapProvider.Instance;
    //是否使用緩存
    GMaps.Instance.Mode = AccessMode.ServerAndCache;
    //設定缓存位置
    gMapControl1.CacheLocation = @"C:\GMapCache";
    //地圖縮放最小比例
    gMapControl1.MinZoom = 1;
    //地圖縮放最大比例
    gMapControl1.MaxZoom = 22;
    //地圖縮放當前比例
    gMapControl1.Zoom = 7;
    //是否顯示中心十字
    gMapControl1.ShowCenter = true;
    //設定地圖拖拽按鍵
    gMapControl1.DragButton = MouseButtons.Right;
    //設定地圖中心經緯度
    gMapControl1.Position = new PointLatLng(24.064, 120.704);

GMap可以任意增加圖層(Overlay),圖層上可增加任意標示(Marker)。

    //增加Marker,有內建的Marker,給予經緯度座標。
    var marker = new GMarkerGoogle(new PointLatLng(24.064, 120.704), GMarkerGoogleType.yellow_small);
    //建立裝載Marker用的Overlay,如果要區分不同Marker,可以建立不同Overlay
    var overlay = new GMapOverlay("marker");
    overlay.Markers.Add(marker);
    gMapControl1.Overlays.Add(overlay);

圖層可增加

  • GMapPolygon
  • GMapRoute
  • GMarkerGoogle

GMapRoute:

    //設定目標點位
    List<PointLatLng> points= new List<PointLatLng>
    {
        new PointLatLng(10,20),
        new PointLatLng(30,40)
    };
    //設定筆刷大小及樣式(預設是實線)
    var pen = new Pen(Color.Red, 2)
    {
        DashStyle = System.Drawing.Drawing2D.DashStyle.Dash//虛線
    };
    //建立Route
    var route = new GMapRoute(points, "route")
    {         Stroke = pen,     };     //建立Overlay     var overlay = new GMapOverlay("routes");     overlay.Routes.Add(route);     gMapControl1.Overlays.Add(overlay);

GMapPolygon:

    與GMapRoute相似,只是不會規劃路線,以及多了封閉選項。
    封閉選項 => 是否要讓最後的點位會連接到第一個點位。

座標轉換:

    //螢幕座標轉經緯度
    var latLng = gMapControl1.FromLocalToLatLng(int x, int y);
    //經緯度轉螢幕座標
    var pos = gMapControl1.FromLatLngToLocal(new PointLatLng(24.064, 120.704));

2022年5月3日 星期二

[Window Form] 載入GMap

使用NuGet下載GMap必需的相關檔案。
    工具 => NuGet套件管理員 => 管理方案的NuGet套件


在瀏覽中搜尋GMap,安裝其中的GMap.NET.Windows,它會自動去下載相依的檔案,
如:GMap.NET.Core、GMap.NET.WinForms、GMap.NET.Presentation...等。


下載完後就可以在設計模式(Design)下的工具箱中找到GMapControl。

拉出GMapControl後還需要設定一些參數才能正確顯示地圖。
還沒設定前的GMapControl:

設定前的參數:

設定 this.gMapControl1.MaxZoom = 22;
         this.gMapControl1.MinZoom = 2;
         this.gMapControl1.Zoom = 5D;
         //使用預設的大小Size(150, 150),可能小於Tile的大小,顯示會不完全
         this.gMapControl1.Size = new System.Drawing.Size(300, 300);
設定後的參數:



在建構式中設定:
    //使用何種地圖
    gMapControl1.MapProvider = GMap.NET.MapProviders.GoogleChinaHybridMapProvider.Instance;
    //是否使用緩存
    GMaps.Instance.Mode = AccessMode.ServerAndCache;
    //設定最初顯示的座標
    gMapControl1.Position = new PointLatLng(24.064, 120.704);
設定前的GMapControl:


2022年4月28日 星期四

[Window Form] 繪製地面站姿態儀(九) 使用SerialPort連接飛控

 我們使用Mission Planner提供的Example來進行Serial Port連線,

並取得飛控中的姿態資訊後與我們之前完成的UI結合在一起。


首先先下載Mission Planner的Source Code

打開Mission Planner的資料夾中的SimpleExample。

路徑 => MissionPlanner-master\ExtLibs\SimpleExample

在我們的UI中增加兩個ComboBox,一個選擇Serial Port,一個選擇BaudRate,

最後再加一個Button1當作Connect。

設定兩個ComboBox,
ComboBox1取得系統上的Serial Port

    private void comboBox1_MouseClick(object sender, MouseEventArgs e)
    {
        //取得系統上的Comport
        comboBox1.DataSource = SerialPort.GetPortNames();
    }

在建構式中設定ComboBox2,手動增加BaudRate

    var list = new List <int> { 19200,115200 };
    comboBox2.DataSource = list;

BaudRate可以參考Mission Planner的常用BaudRate,

但實際值還是要看飛控上的設定為主。


載入MAVLink:
    在SimpleExample執行後會產生一個exe檔與相關的dll,
    我們只需要將生成的MAVLink.dll引入到專案中就可以使用MAVLink了。
    參考 => (右鍵)加入參考 => 瀏覽 =>
    (下載的SourceCode => MissionPlanner-master\ExtLibs\SimpleExample\bin\Debug)
    選取 MAVLink.dll => 確定




設定Button

    private void Button1_Click(object sender, EventArgs e)
    {
        if (serialPort1.IsOpen)
            serialPort1.Close();
        serialPort1.PortName = comboBox1.Text;
        serialPort1.BaudRate = int.Parse(comboBox2.Text);
        serialPort1.Open();
        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += Worker_DoWork;
        worker.RunWorkerAsync();
    }

    private MAVLink.MavlinkParse mavlink = new MAVLink.MavlinkParse();
    private object readLock = new object();
    private byte sysid;
    private byte compid;

    private void Worker_DoWork(object sender, DoWorkEventArgs e)
    {
        while (serialPort1.IsOpen)
        {
            try
            {
                MAVLink.MAVLinkMessage packet;
                lock (readLock)
                {
                    packet = mavlink.ReadPacket(serialPort1.BaseStream);
                    if (packet == null || packet.data == null) continue;
                }

                if (packet.data.GetType() == typeof(MAVLink.mavlink_heartbeat_t))
                {
                    var hb = (MAVLink.mavlink_heartbeat_t)packet.data;

                    // save the sysid and compid of the seen MAV
                    sysid = packet.sysid;
                    compid = packet.compid;

                    // request streams at 2 hz
                    var buffer = mavlink.GenerateMAVLinkPacket10(MAVLink.MAVLINK_MSG_ID.REQUEST_DATA_STREAM,
                        new MAVLink.mavlink_request_data_stream_t()
                    {
                        req_message_rate = 2,
                        req_stream_id = (byte)MAVLink.MAV_DATA_STREAM.ALL,
                        start_stop = 1,
                        target_component = compid,
                        target_system = sysid
                    });

                    serialPort1.Write(buffer, 0, buffer.Length);

                    buffer = mavlink.GenerateMAVLinkPacket10(MAVLink.MAVLINK_MSG_ID.HEARTBEAT, hb);

                    serialPort1.Write(buffer, 0, buffer.Length);
                }

                if (sysid != packet.sysid || compid != packet.compid)
                    continue;

                //Console.WriteLine(packet.msgtypename);

                if (packet.msgid == (byte)MAVLink.MAVLINK_MSG_ID.ATTITUDE)
                //or
                //if (packet.data.GetType() == typeof(MAVLink.mavlink_attitude_t))
                {
                    var att = (MAVLink.mavlink_attitude_t)packet.data;
                    var roll = att.roll * 57.2958;
                    var yaw = att.yaw * 57.2958;
                    var pitch = att.pitch * 57.2958;
                    userControl1.Roll = (float)roll;
                    userControl1.Yaw = (float)yaw;
                    userControl1.Pitch = (float)pitch;
                    //Console.WriteLine($@"roll => {roll}  yaw => {yaw}  pitch => {pitch}");
                }
            }
            catch (Exception)
            {
                throw;
            }
            Thread.Sleep(1);
        }
    }